using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Deployment;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;

namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests;

/// <summary>
/// Coverage for the startup-reconciliation repo methods on <see cref="DeploymentManagerRepository"/>:
/// <see cref="IDeploymentManagerRepository.GetExpectedDeploymentsForSiteAsync"/> (central's deployed
/// set for a site, excluding instances with no snapshot) and
/// <see cref="IDeploymentManagerRepository.StagePendingIfAbsentAsync"/> (insert-if-absent, used by
/// the reconcile handler to mint fresh fetch tokens for the gap without clobbering an in-flight deploy).
/// Uses the shared SQLite in-memory fixture with real FK enforcement.
/// </summary>
public class ReconcileRepositoryTests : IDisposable
{
    private readonly ScadaBridgeDbContext _context;
    private readonly DeploymentManagerRepository _repository;

    public ReconcileRepositoryTests()
    {
        _context = SqliteTestHelper.CreateInMemoryContext();
        _repository = new DeploymentManagerRepository(_context);
    }

    public void Dispose()
    {
        _context.Database.CloseConnection();
        _context.Dispose();
    }

    // --- Seed helpers ---

    /// <summary>Seeds a Site row and returns its generated id.</summary>
    private async Task<int> SeedSiteAsync(string name)
    {
        var site = new Site(name, $"SI-{name}");
        _context.Sites.Add(site);
        await _context.SaveChangesAsync();
        return site.Id;
    }

    /// <summary>Seeds a Template + Instance on the given site and returns the instance id.</summary>
    private async Task<int> SeedInstanceAsync(string uniqueName, int siteId, InstanceState state = InstanceState.Enabled)
    {
        var template = new Template($"T-{uniqueName}");
        _context.Templates.Add(template);
        await _context.SaveChangesAsync();

        var instance = new Instance(uniqueName) { SiteId = siteId, TemplateId = template.Id, State = state };
        _context.Instances.Add(instance);
        await _context.SaveChangesAsync();
        return instance.Id;
    }

    /// <summary>Seeds a <see cref="DeployedConfigSnapshot"/> for the given instance and returns it.</summary>
    private async Task<DeployedConfigSnapshot> SeedSnapshotAsync(int instanceId, string deploymentId, string revisionHash)
    {
        var snapshot = new DeployedConfigSnapshot(deploymentId, revisionHash, "{}") { InstanceId = instanceId };
        _context.Set<DeployedConfigSnapshot>().Add(snapshot);
        await _context.SaveChangesAsync();
        return snapshot;
    }

    // --- GetExpectedDeploymentsForSiteAsync ---

    [Fact]
    public async Task GetExpectedDeployments_ReturnsOnlyDeployedInstancesForSite()
    {
        // Arrange: site A has two deployed instances (one Enabled, one Disabled)
        //          site B has one deployed instance
        //          site A also has one instance WITHOUT a snapshot (must be excluded)
        var siteAId = await SeedSiteAsync("SiteA");
        var siteBId = await SeedSiteAsync("SiteB");

        var instA1Id = await SeedInstanceAsync("A-Inst1", siteAId, InstanceState.Enabled);
        var instA2Id = await SeedInstanceAsync("A-Inst2", siteAId, InstanceState.Disabled);
        var instA3Id = await SeedInstanceAsync("A-Inst3-NoSnap", siteAId, InstanceState.Enabled);
        var instB1Id = await SeedInstanceAsync("B-Inst1", siteBId, InstanceState.Enabled);

        await SeedSnapshotAsync(instA1Id, "dep-a1", "hash-a1");
        await SeedSnapshotAsync(instA2Id, "dep-a2", "hash-a2");
        // instA3 intentionally has no snapshot
        await SeedSnapshotAsync(instB1Id, "dep-b1", "hash-b1");

        // Act
        var result = await _repository.GetExpectedDeploymentsForSiteAsync(siteAId);

        // Assert: exactly the two site-A instances that have snapshots
        Assert.Equal(2, result.Count);

        var byName = result.ToDictionary(r => r.InstanceUniqueName);
        Assert.True(byName.ContainsKey("A-Inst1"), "A-Inst1 should be present");
        Assert.True(byName.ContainsKey("A-Inst2"), "A-Inst2 should be present");
        Assert.False(byName.ContainsKey("A-Inst3-NoSnap"), "instance without snapshot must be excluded");
        Assert.False(byName.ContainsKey("B-Inst1"), "site B instance must be excluded");

        // Check field correctness for A-Inst1 (Enabled)
        var a1 = byName["A-Inst1"];
        Assert.Equal(instA1Id, a1.InstanceId);
        Assert.Equal("dep-a1", a1.DeploymentId);
        Assert.Equal("hash-a1", a1.RevisionHash);
        Assert.True(a1.IsEnabled);

        // Check field correctness for A-Inst2 (Disabled)
        var a2 = byName["A-Inst2"];
        Assert.Equal(instA2Id, a2.InstanceId);
        Assert.Equal("dep-a2", a2.DeploymentId);
        Assert.Equal("hash-a2", a2.RevisionHash);
        Assert.False(a2.IsEnabled);
    }

    [Fact]
    public async Task GetExpectedDeployments_NoDeployedInstances_ReturnsEmpty()
    {
        var siteId = await SeedSiteAsync("EmptySite");
        // Seed an instance but no snapshot
        await SeedInstanceAsync("Inst-NoSnap", siteId);

        var result = await _repository.GetExpectedDeploymentsForSiteAsync(siteId);

        Assert.Empty(result);
    }

    // --- StagePendingIfAbsentAsync ---

    [Fact]
    public async Task StagePendingIfAbsent_NoPriorRow_StagesAndReturnsTrue()
    {
        // Arrange
        var siteId = await SeedSiteAsync("SiteX");
        var instanceId = await SeedInstanceAsync("InstX", siteId);
        var now = DateTimeOffset.UtcNow;

        // Act
        var staged = await _repository.StagePendingIfAbsentAsync(
            instanceId,
            deploymentId: "dep-new",
            revisionHash: "rev-new",
            configurationJson: "{\"x\":1}",
            token: "tok-new",
            createdAtUtc: now,
            expiresAtUtc: now.AddMinutes(10));

        // Assert: staged, and the row is retrievable with the correct token + config
        Assert.True(staged);
        var row = await _repository.GetPendingDeploymentByIdAsync("dep-new");
        Assert.NotNull(row);
        Assert.Equal(instanceId, row!.InstanceId);
        Assert.Equal("tok-new", row.Token);
        Assert.Equal("{\"x\":1}", row.ConfigurationJson);
        Assert.Equal("rev-new", row.RevisionHash);
    }

    [Fact]
    public async Task StagePendingIfAbsent_PriorRowExists_ReturnsFalseAndDoesNotOverwrite()
    {
        // Arrange: an existing pending row for the instance (simulates an in-flight deploy)
        var siteId = await SeedSiteAsync("SiteY");
        var instanceId = await SeedInstanceAsync("InstY", siteId);
        var now = DateTimeOffset.UtcNow;

        var existing = new PendingDeployment(
            "dep-existing", instanceId, revisionHash: "rev-existing",
            configurationJson: "{\"existing\":true}", token: "tok-existing",
            createdAtUtc: now.AddMinutes(-5), expiresAtUtc: now.AddMinutes(5));
        _context.Set<PendingDeployment>().Add(existing);
        await _context.SaveChangesAsync();

        // Act: try to stage a newer row for the same instance
        var staged = await _repository.StagePendingIfAbsentAsync(
            instanceId,
            deploymentId: "dep-reconcile",
            revisionHash: "rev-reconcile",
            configurationJson: "{\"reconcile\":true}",
            token: "tok-reconcile",
            createdAtUtc: now,
            expiresAtUtc: now.AddMinutes(10));

        // Assert: skipped (returns false), existing row is unchanged
        Assert.False(staged);

        var existingRow = await _repository.GetPendingDeploymentByIdAsync("dep-existing");
        Assert.NotNull(existingRow);
        Assert.Equal("tok-existing", existingRow!.Token);
        Assert.Equal("{\"existing\":true}", existingRow.ConfigurationJson);

        // The reconcile row must not exist
        Assert.Null(await _repository.GetPendingDeploymentByIdAsync("dep-reconcile"));
    }
}
