using System.IO.Compression;
using System.Text;
using System.Text.Json;
using EntKube.Web.Data;
using EntKube.Web.Services;
using FluentAssertions;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;

namespace EntKube.Web.Tests;

/// <summary>
/// Tests for ComponentScanService — verifies Helm release decoding, discovery
/// logic, and import behavior. Uses SQLite in-memory for isolated database tests.
/// </summary>
public class ComponentScanServiceTests : IDisposable
{
    private static readonly byte[] TestRootKey = Convert.FromBase64String(
        "dGhpcyBpcyBhIDMyIGJ5dGUga2V5ISEhMTIzNDU2Nzg=");

    private readonly SqliteConnection connection;
    private readonly ApplicationDbContext db;
    private readonly TestDbContextFactory dbFactory;
    private readonly ComponentScanService sut;
    private readonly Guid tenantId = Guid.NewGuid();
    private readonly Guid environmentId = Guid.NewGuid();
    private readonly Guid clusterId = Guid.NewGuid();
    private readonly KubernetesCluster testCluster;

    public ComponentScanServiceTests()
    {
        connection = new SqliteConnection("DataSource=:memory:");
        connection.Open();

        DbContextOptions<ApplicationDbContext> options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseSqlite(connection)
            .Options;

        db = new ApplicationDbContext(options);
        dbFactory = new TestDbContextFactory(connection);
        db.Database.EnsureCreated();

        VaultEncryptionService encryption = new(TestRootKey);
        VaultService vaultService = new(dbFactory, encryption);
        sut = new ComponentScanService(dbFactory, vaultService);

        // Seed tenant, environment, and cluster.

        db.Tenants.Add(new Tenant { Id = tenantId, Name = "TestTenant", Slug = "test" });
        db.Environments.Add(new Data.Environment { Id = environmentId, TenantId = tenantId, Name = "dev" });

        testCluster = new KubernetesCluster
        {
            Id = clusterId,
            TenantId = tenantId,
            EnvironmentId = environmentId,
            Name = "test-cluster",
            ApiServerUrl = "https://k8s.example.com"
        };

        db.KubernetesClusters.Add(testCluster);
        db.SaveChanges();
    }

    public void Dispose()
    {
        db.Dispose();
        connection.Dispose();
    }

    // ──────── DecodeHelmRelease Tests ────────

    [Fact]
    public void DecodeHelmRelease_ValidPayload_ReturnsRelease()
    {
        // Arrange — create a realistic Helm release JSON, gzip it, base64 it.

        HelmReleaseJson releaseJson = new()
        {
            Name = "my-app",
            Namespace = "production",
            Chart = new HelmChartJson
            {
                Metadata = new HelmChartMetadataJson
                {
                    Name = "nginx",
                    Version = "1.2.3",
                    AppVersion = "1.25.0"
                }
            },
            Info = new HelmInfoJson
            {
                Status = "deployed",
                LastDeployed = "2025-01-15T10:30:00Z"
            },
            Config = JsonDocument.Parse("{\"replicaCount\":3,\"image\":{\"tag\":\"latest\"}}").RootElement
        };

        byte[] secretData = EncodeHelmRelease(releaseJson);

        // Act — invoke the private method via the public ScanHelmReleasesAsync,
        // but since we can't easily mock K8s client, test the decode logic directly.

        DiscoveredHelmRelease? result = InvokeDecodeHelmRelease(secretData, 5);

        // Assert

        result.Should().NotBeNull();
        result!.Name.Should().Be("my-app");
        result.Namespace.Should().Be("production");
        result.ChartName.Should().Be("nginx");
        result.ChartVersion.Should().Be("1.2.3");
        result.AppVersion.Should().Be("1.25.0");
        result.Status.Should().Be("deployed");
        result.Revision.Should().Be(5);
        result.Values.Should().Contain("replicaCount");
        result.UpdatedAt.Should().NotBeNull();
    }

    [Fact]
    public void DecodeHelmRelease_EmptyConfig_ReturnsNullValues()
    {
        // Arrange — release with empty config {}.

        HelmReleaseJson releaseJson = new()
        {
            Name = "minimal",
            Namespace = "default",
            Chart = new HelmChartJson
            {
                Metadata = new HelmChartMetadataJson { Name = "redis", Version = "0.1.0" }
            },
            Info = new HelmInfoJson { Status = "deployed" },
            Config = JsonDocument.Parse("{}").RootElement
        };

        byte[] secretData = EncodeHelmRelease(releaseJson);

        // Act

        DiscoveredHelmRelease? result = InvokeDecodeHelmRelease(secretData, 1);

        // Assert — empty config means no custom values.

        result.Should().NotBeNull();
        result!.Values.Should().BeNull();
    }

    [Fact]
    public void DecodeHelmRelease_InvalidData_ReturnsFallbackFromLabels()
    {
        // Arrange — garbage data that isn't valid base64 of gzipped content.

        byte[] secretData = Encoding.UTF8.GetBytes("not-valid-base64!!!");

        // Act — the service gracefully falls back to label-based metadata.

        DiscoveredHelmRelease? result = InvokeDecodeHelmRelease(secretData, 1);

        // Assert — returns a minimal release from secret labels.

        result.Should().NotBeNull();
        result!.Name.Should().Be("test");
        result.Namespace.Should().Be("default");
        result.Revision.Should().Be(1);
    }

    // ──────── ImportReleaseAsync Tests ────────

    [Fact]
    public async Task ImportReleaseAsync_NewRelease_CreatesComponent()
    {
        // Arrange

        DiscoveredHelmRelease release = new()
        {
            Name = "grafana",
            Namespace = "monitoring",
            ChartName = "grafana",
            ChartVersion = "7.0.0",
            Status = "deployed",
            Revision = 3,
            Values = "{\"persistence\":{\"enabled\":true}}",
            UpdatedAt = DateTime.UtcNow
        };

        // Act

        ClusterComponent result = await sut.ImportReleaseAsync(testCluster, release);

        // Assert

        result.Name.Should().Be("grafana");
        result.Namespace.Should().Be("monitoring");
        result.HelmChartName.Should().Be("grafana");
        result.HelmChartVersion.Should().Be("7.0.0");
        result.Status.Should().Be(ComponentStatus.Installed);
        result.HelmValues.Should().Contain("persistence");
        result.ClusterId.Should().Be(clusterId);

        // Verify persisted in DB.

        ClusterComponent? persisted = await db.ClusterComponents.FindAsync(result.Id);
        persisted.Should().NotBeNull();
    }

    [Fact]
    public async Task ImportReleaseAsync_ExistingComponent_UpdatesIt()
    {
        // Arrange — pre-create a component with outdated info.

        ClusterComponent existing = new()
        {
            Id = Guid.NewGuid(),
            ClusterId = clusterId,
            Name = "prometheus",
            ComponentType = "HelmChart",
            Namespace = "old-namespace",
            HelmChartVersion = "1.0.0",
            Status = ComponentStatus.NotInstalled
        };
        db.ClusterComponents.Add(existing);
        await db.SaveChangesAsync();

        DiscoveredHelmRelease release = new()
        {
            Name = "prometheus",
            Namespace = "monitoring",
            ChartName = "kube-prometheus-stack",
            ChartVersion = "55.0.0",
            Status = "deployed",
            Revision = 12,
            Values = "{\"alertmanager\":{\"enabled\":false}}"
        };

        // Act

        ClusterComponent result = await sut.ImportReleaseAsync(testCluster, release);

        // Assert — should update existing, not create new.

        result.Id.Should().Be(existing.Id);
        result.Namespace.Should().Be("monitoring");
        result.HelmChartVersion.Should().Be("55.0.0");
        result.HelmChartName.Should().Be("kube-prometheus-stack");
        result.Status.Should().Be(ComponentStatus.Installed);

        int count = await db.ClusterComponents.CountAsync(c => c.ClusterId == clusterId && c.Name == "prometheus");
        count.Should().Be(1);
    }

    [Fact]
    public async Task ImportAllNewReleasesAsync_SkipsAlreadyTracked()
    {
        // Arrange — mix of tracked and new releases.

        List<DiscoveredHelmRelease> releases =
        [
            new() { Name = "existing-one", Namespace = "ns1", Status = "deployed", Revision = 1, AlreadyTracked = true },
            new() { Name = "new-one", Namespace = "ns2", ChartName = "new-chart", ChartVersion = "1.0.0", Status = "deployed", Revision = 1, AlreadyTracked = false },
            new() { Name = "new-two", Namespace = "ns3", ChartName = "other-chart", ChartVersion = "2.0.0", Status = "deployed", Revision = 1, AlreadyTracked = false }
        ];

        // Act

        int imported = await sut.ImportAllNewReleasesAsync(testCluster, releases);

        // Assert — only non-tracked releases should be imported.

        imported.Should().Be(2);
        int total = await db.ClusterComponents.CountAsync(c => c.ClusterId == clusterId);
        total.Should().Be(2);
    }

    // ──────── MapStatus Tests ────────

    [Theory]
    [InlineData("deployed", ComponentStatus.Installed)]
    [InlineData("failed", ComponentStatus.Failed)]
    [InlineData("pending-install", ComponentStatus.Installing)]
    [InlineData("pending-upgrade", ComponentStatus.Installing)]
    [InlineData("pending-rollback", ComponentStatus.Installing)]
    [InlineData("uninstalling", ComponentStatus.Uninstalling)]
    [InlineData("unknown-status", ComponentStatus.NotInstalled)]
    [InlineData(null, ComponentStatus.NotInstalled)]
    public void MapStatus_MapsHelmStatusToComponentStatus(string? helmStatus, ComponentStatus expected)
    {
        // Use reflection to test the private static method.

        System.Reflection.MethodInfo? method = typeof(ComponentScanService)
            .GetMethod("MapStatus", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);

        method.Should().NotBeNull();

        ComponentStatus result = (ComponentStatus)method!.Invoke(null, [helmStatus])!;

        result.Should().Be(expected);
    }

    // ──────── Helpers ────────

    /// <summary>
    /// Encodes a Helm release JSON object into the format stored in K8s secrets:
    /// JSON → gzip → base64 (Helm). The K8s layer handles the outer base64.
    /// </summary>
    private static byte[] EncodeHelmRelease(HelmReleaseJson release)
    {
        string json = JsonSerializer.Serialize(release, new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
        });

        byte[] jsonBytes = Encoding.UTF8.GetBytes(json);

        using MemoryStream compressedStream = new();
        using (GZipStream gzipStream = new(compressedStream, CompressionMode.Compress))
        {
            gzipStream.Write(jsonBytes);
        }

        // Helm base64 encoding.

        string helmBase64 = Convert.ToBase64String(compressedStream.ToArray());
        return Encoding.UTF8.GetBytes(helmBase64);
    }

    /// <summary>
    /// Invokes the private DecodeHelmRelease method via reflection for unit testing.
    /// </summary>
    private static DiscoveredHelmRelease? InvokeDecodeHelmRelease(byte[] secretData, int revision)
    {
        System.Reflection.MethodInfo? method = typeof(ComponentScanService)
            .GetMethod("DecodeHelmRelease", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);

        if (method is null)
        {
            throw new InvalidOperationException("DecodeHelmRelease method not found");
        }

        // Build a minimal V1Secret with the release data.

        k8s.Models.V1Secret secret = new()
        {
            Metadata = new k8s.Models.V1ObjectMeta
            {
                Name = "sh.helm.release.v1.test.v1",
                NamespaceProperty = "default",
                Labels = new Dictionary<string, string>
                {
                    ["name"] = "test",
                    ["owner"] = "helm",
                    ["version"] = revision.ToString()
                }
            },
            Data = new Dictionary<string, byte[]>
            {
                ["release"] = secretData
            }
        };

        return method.Invoke(null, [secret, revision]) as DiscoveredHelmRelease;
    }

    // ──────── Test DTOs for creating Helm release JSON ────────

    private class HelmReleaseJson
    {
        public string Name { get; set; } = "";
        public string Namespace { get; set; } = "";
        public HelmChartJson? Chart { get; set; }
        public HelmInfoJson? Info { get; set; }
        public JsonElement? Config { get; set; }
    }

    private class HelmChartJson
    {
        public HelmChartMetadataJson? Metadata { get; set; }
    }

    private class HelmChartMetadataJson
    {
        public string? Name { get; set; }
        public string? Version { get; set; }
        public string? AppVersion { get; set; }
    }

    private class HelmInfoJson
    {
        public string? Status { get; set; }

        [System.Text.Json.Serialization.JsonPropertyName("last_deployed")]
        public string? LastDeployed { get; set; }
    }
}
