using NSubstitute;
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.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening;
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;

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

/// <summary>
/// WP-8: Tests for deployed vs template-derived state comparison.
///
/// PLAN-05 Task 16: adds behavioral coverage proving the read-only
/// staleness/comparison path can flatten (and thus hash) an instance whose
/// script does NOT compile — because <c>validateScripts: false</c> skips the
/// expensive Roslyn <see cref="ValidationService.ValidateScriptCompilation"/>
/// step while keeping structural/semantic validation. The default (deploy) path
/// still compiles.
/// </summary>
public class DeploymentComparisonTests
{
    [Fact]
    public void DeploymentComparisonResult_MatchingHashes_NotStale()
    {
        var result = new DeploymentComparisonResult(
            1, "sha256:abc", "sha256:abc", false, DateTimeOffset.UtcNow);

        Assert.False(result.IsStale);
        Assert.Equal("sha256:abc", result.DeployedRevisionHash);
        Assert.Equal("sha256:abc", result.CurrentRevisionHash);
    }

    [Fact]
    public void DeploymentComparisonResult_DifferentHashes_IsStale()
    {
        var result = new DeploymentComparisonResult(
            1, "sha256:old", "sha256:new", true, DateTimeOffset.UtcNow);

        Assert.True(result.IsStale);
        Assert.NotEqual(result.DeployedRevisionHash, result.CurrentRevisionHash);
    }

    [Fact]
    public void DeploymentComparisonResult_ContainsDeployedTimestamp()
    {
        var deployedAt = new DateTimeOffset(2026, 3, 16, 12, 0, 0, TimeSpan.Zero);
        var result = new DeploymentComparisonResult(1, "h1", "h2", true, deployedAt);

        Assert.Equal(deployedAt, result.DeployedAt);
    }

    // ── PLAN-05 Task 16: read-only paths skip Roslyn script compilation ──

    private const int InstanceId = 1;
    private const int TemplateId = 10;
    private const int SiteId = 100;

    // A deliberately non-compiling script body. The deploy gate
    // (validateScripts: true) flags this as a ScriptCompilation error; the
    // read-only staleness/comparison path (validateScripts: false) must skip
    // the compile entirely and still produce a flattened config + hash.
    private const string NonCompilingScript = "int x = ;";

    private static FlatteningPipeline BuildPipeline(
        ITemplateEngineRepository templateRepo,
        ISiteRepository siteRepo,
        ISharedSchemaRepository sharedSchemaRepo)
    {
        sharedSchemaRepo.ListAsync(Arg.Any<CancellationToken>()).Returns([]);
        return new FlatteningPipeline(
            templateRepo,
            siteRepo,
            new FlatteningService(),
            new ValidationService(),
            new RevisionHashService(),
            sharedSchemaRepo,
            new TemplateGraphWatermark());
    }

    private static (ITemplateEngineRepository, ISiteRepository, ISharedSchemaRepository) ArrangeNonCompilingScript()
    {
        var templateRepo = Substitute.For<ITemplateEngineRepository>();
        var siteRepo = Substitute.For<ISiteRepository>();
        var sharedSchemaRepo = Substitute.For<ISharedSchemaRepository>();

        var template = new Template("Tank") { Id = TemplateId };
        template.Scripts.Add(new TemplateScript("BadScript", NonCompilingScript));

        var instance = new Instance("Tank-01") { Id = InstanceId, TemplateId = TemplateId, SiteId = SiteId };

        templateRepo.GetInstanceByIdAsync(InstanceId, Arg.Any<CancellationToken>()).Returns(instance);
        templateRepo.GetTemplateWithChildrenAsync(TemplateId, Arg.Any<CancellationToken>()).Returns(template);
        templateRepo.GetCompositionsByTemplateIdAsync(TemplateId, Arg.Any<CancellationToken>()).Returns([]);
        templateRepo.GetAllSharedScriptsAsync(Arg.Any<CancellationToken>()).Returns([]);
        siteRepo.GetDataConnectionsBySiteIdAsync(SiteId, Arg.Any<CancellationToken>()).Returns([]);

        return (templateRepo, siteRepo, sharedSchemaRepo);
    }

    [Fact]
    public async Task FlattenAndValidate_ValidateScriptsFalse_NonCompilingScript_SucceedsWithHashAndNoCompilationError()
    {
        var (templateRepo, siteRepo, sharedSchemaRepo) = ArrangeNonCompilingScript();
        var sut = BuildPipeline(templateRepo, siteRepo, sharedSchemaRepo);

        var result = await sut.FlattenAndValidateAsync(InstanceId, validateScripts: false);

        Assert.True(result.IsSuccess);
        Assert.False(string.IsNullOrEmpty(result.Value.RevisionHash));
        // The Roslyn compile step was skipped, so no ScriptCompilation error is raised
        // even though the script body does not compile.
        Assert.DoesNotContain(result.Value.Validation.Errors,
            e => e.Category == ValidationCategory.ScriptCompilation);
    }

    [Fact]
    public async Task FlattenAndValidate_DefaultValidateScripts_NonCompilingScript_ReportsCompilationError()
    {
        var (templateRepo, siteRepo, sharedSchemaRepo) = ArrangeNonCompilingScript();
        var sut = BuildPipeline(templateRepo, siteRepo, sharedSchemaRepo);

        // Default (deploy) path — compilation is the authoritative gate.
        var result = await sut.FlattenAndValidateAsync(InstanceId);

        Assert.True(result.IsSuccess);
        Assert.Contains(result.Value.Validation.Errors,
            e => e.Category == ValidationCategory.ScriptCompilation);
    }
}
