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.Enums;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
using ZB.MOM.WW.ScadaBridge.DeploymentManager;
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>
/// M2.8 (#23): proves the deploy path (FlatteningPipeline.FlattenAndValidateAsync)
/// opts into connection-binding enforcement, so a data-sourced attribute with no
/// binding gates the deployment as an ERROR (not just a warning), and that a binding
/// resolving to a connection that actually exists at the target site passes.
/// </summary>
public class FlatteningPipelineConnectionBindingTests
{
    private const int InstanceId = 1;
    private const int TemplateId = 10;
    private const int SiteId = 100;
    private const int ConnectionId = 7;

    private readonly ITemplateEngineRepository _templateRepo = Substitute.For<ITemplateEngineRepository>();
    private readonly ISiteRepository _siteRepo = Substitute.For<ISiteRepository>();
    private readonly ISharedSchemaRepository _sharedSchemaRepo = Substitute.For<ISharedSchemaRepository>();
    private readonly FlatteningPipeline _sut;

    public FlatteningPipelineConnectionBindingTests()
    {
        _sharedSchemaRepo.ListAsync(Arg.Any<CancellationToken>())
            .Returns([]);

        _sut = new FlatteningPipeline(
            _templateRepo,
            _siteRepo,
            new FlatteningService(),
            new ValidationService(),
            new RevisionHashService(),
            _sharedSchemaRepo,
            new TemplateGraphWatermark());
    }

    /// <summary>
    /// Seeds a single-template chain with one data-sourced attribute ("Temp") and a
    /// site that owns a single "PlantBus" data connection. The instance optionally
    /// binds "Temp" to <paramref name="boundConnectionId"/>.
    /// </summary>
    private void Arrange(int? boundConnectionId)
    {
        var template = new Template("Tank") { Id = TemplateId };
        template.Attributes.Add(new TemplateAttribute("Temp")
        {
            DataType = DataType.Double,
            DataSourceReference = "ns=2;s=Temp"
        });

        var instance = new Instance("Tank-01") { Id = InstanceId, TemplateId = TemplateId, SiteId = SiteId };
        if (boundConnectionId.HasValue)
        {
            instance.ConnectionBindings.Add(new InstanceConnectionBinding("Temp")
            {
                InstanceId = InstanceId,
                DataConnectionId = boundConnectionId.Value
            });
        }

        _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([]);

        var connection = new DataConnection("PlantBus", "OpcUa", SiteId) { Id = ConnectionId };
        _siteRepo.GetDataConnectionsBySiteIdAsync(SiteId, Arg.Any<CancellationToken>())
            .Returns([connection]);
    }

    [Fact]
    public async Task FlattenAndValidate_DataSourcedAttributeWithNoBinding_ReportsBindingError()
    {
        Arrange(boundConnectionId: null);

        var result = await _sut.FlattenAndValidateAsync(InstanceId);

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

    [Fact]
    public async Task FlattenAndValidate_BindingToExistingSiteConnection_NoBindingError()
    {
        Arrange(boundConnectionId: ConnectionId);

        var result = await _sut.FlattenAndValidateAsync(InstanceId);

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

    [Fact]
    public async Task FlattenAndValidate_BindingToStaleDeletedConnection_ReportsBindingError()
    {
        // M2.8 (#23): FlatteningService.ApplyConnectionBindings silently drops a
        // binding whose DataConnectionId doesn't resolve to any loaded site
        // DataConnection (stale / deleted connection). The flattener leaves
        // BoundDataConnectionId == null, so the validator treats the attribute as
        // unbound and gates the deployment with a ConnectionBinding Error.
        //
        // Arrange: the instance binding points at id 999, but the site only has
        // the connection with id=ConnectionId (7). The flattener can't resolve 999
        // and drops the binding silently; the validator then flags it.
        const int StaleConnectionId = 999;
        Arrange(boundConnectionId: StaleConnectionId);

        var result = await _sut.FlattenAndValidateAsync(InstanceId);

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