using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;

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

public class SchemaConfigurationTests : IDisposable
{
    private readonly ScadaBridgeDbContext _context;

    public SchemaConfigurationTests()
    {
        _context = SqliteTestHelper.CreateInMemoryContext();
    }

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

    // ConfigurationDatabase-006: the gRPC node-address columns must be length-bounded
    // (HasMaxLength(500)) consistently with the sibling NodeAAddress/NodeBAddress columns,
    // rather than being left to map to nvarchar(max).

    [Theory]
    [InlineData(nameof(Site.GrpcNodeAAddress))]
    [InlineData(nameof(Site.GrpcNodeBAddress))]
    public void GrpcNodeAddressColumns_AreLengthBoundedTo500(string propertyName)
    {
        var property = _context.Model
            .FindEntityType(typeof(Site))!
            .FindProperty(propertyName)!;

        Assert.Equal(500, property.GetMaxLength());
    }

    [Theory]
    [InlineData(nameof(Site.NodeAAddress))]
    [InlineData(nameof(Site.NodeBAddress))]
    public void GrpcNodeAddressColumns_MatchSiblingNodeAddressBounds(string siblingPropertyName)
    {
        var entity = _context.Model.FindEntityType(typeof(Site))!;
        var siblingMaxLength = entity.FindProperty(siblingPropertyName)!.GetMaxLength();

        Assert.Equal(siblingMaxLength, entity.FindProperty(nameof(Site.GrpcNodeAAddress))!.GetMaxLength());
        Assert.Equal(siblingMaxLength, entity.FindProperty(nameof(Site.GrpcNodeBAddress))!.GetMaxLength());
    }

    // ConfigurationDatabase-014: the encrypting value converter must be applied
    // uniformly to all three secret-bearing columns, including the non-nullable
    // DatabaseConnectionDefinition.ConnectionString. A regression here (e.g. the
    // converter dropped from one HasConversion call) would silently store a secret
    // in plaintext.

    [Theory]
    [InlineData(typeof(SmtpConfiguration), nameof(SmtpConfiguration.Credentials))]
    [InlineData(typeof(ExternalSystemDefinition), nameof(ExternalSystemDefinition.AuthConfiguration))]
    [InlineData(typeof(DatabaseConnectionDefinition), nameof(DatabaseConnectionDefinition.ConnectionString))]
    public void SecretColumns_AllHaveEncryptedStringConverterApplied(Type entityType, string propertyName)
    {
        var converter = _context.Model
            .FindEntityType(entityType)!
            .FindProperty(propertyName)!
            .GetValueConverter();

        Assert.IsType<EncryptedStringConverter>(converter);
    }

    // arch-review 04 (P1): the SiteCalls "live queue" KPI predicates all filter on
    // TerminalAtUtc IS NULL and run every 60 s (KPI recorder) + every 10 s (Health poll).
    // A filtered index over just the non-terminal rows turns each count from a clustered
    // scan of the 365-day archive into a small seek. Locked shape:
    // key = CreatedAtUtc, filter = [TerminalAtUtc] IS NULL, INCLUDE SourceSite/SourceNode/Status.
    [Fact]
    public void SiteCalls_Has_Filtered_NonTerminal_Index()
    {
        // IncludeProperties is a SQL-Server-only annotation stripped from the runtime
        // read-optimized model, so read the design-time model where it is preserved.
        var model = _context.GetService<IDesignTimeModel>().Model;
        var entity = model.FindEntityType(typeof(SiteCall))!;
        var index = entity.GetIndexes().Single(i => i.GetDatabaseName() == "IX_SiteCalls_NonTerminal");

        Assert.Equal(new[] { nameof(SiteCall.CreatedAtUtc) }, index.Properties.Select(p => p.Name));
        Assert.Equal("[TerminalAtUtc] IS NULL", index.GetFilter());
        Assert.Equal(
            new[] { nameof(SiteCall.SourceSite), nameof(SiteCall.SourceNode), nameof(SiteCall.Status) },
            index.GetIncludeProperties());
    }

    // arch-review 04 round 2 (R6): the daily terminal retention purge filters on
    // TerminalAtUtc IS NOT NULL AND TerminalAtUtc < cutoff. A filtered index over the
    // terminal population lets the purge (and Task 11's MIN() slice anchor) seek the
    // expired tail instead of full-scanning the 365-day table. Locked shape:
    // key = TerminalAtUtc, filter = [TerminalAtUtc] IS NOT NULL.
    [Fact]
    public void SiteCalls_Has_Filtered_Terminal_Index()
    {
        var model = _context.GetService<IDesignTimeModel>().Model;
        var entity = model.FindEntityType(typeof(SiteCall))!;
        var index = entity.GetIndexes().Single(i => i.GetDatabaseName() == "IX_SiteCalls_Terminal");

        Assert.Equal(new[] { nameof(SiteCall.TerminalAtUtc) }, index.Properties.Select(p => p.Name).ToArray());
        Assert.Equal("[TerminalAtUtc] IS NOT NULL", index.GetFilter());
    }
}

public class SplitQueryBehaviourTests : IDisposable
{
    private readonly ScadaBridgeDbContext _context;
    private readonly TemplateEngineRepository _repository;

    public SplitQueryBehaviourTests()
    {
        _context = SqliteTestHelper.CreateInMemoryContext();
        _repository = new TemplateEngineRepository(_context, new TemplateGraphWatermark());
    }

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

    // ConfigurationDatabase-009: the multi-collection eager-load queries were switched to
    // AsSplitQuery() to avoid cartesian-product joins. The result set must be unchanged —
    // every member collection still fully populated, with no row duplication.

    [Fact]
    public async Task GetAllTemplatesAsync_WithMultipleMembersPerCollection_LoadsAllWithoutDuplication()
    {
        var template = new Template("MultiMember");
        for (int i = 0; i < 3; i++)
            template.Attributes.Add(new TemplateAttribute($"Attr{i}"));
        for (int i = 0; i < 2; i++)
            template.Alarms.Add(new TemplateAlarm($"Alarm{i}"));
        for (int i = 0; i < 4; i++)
            template.Scripts.Add(new TemplateScript($"Script{i}", "return 1;"));
        _context.Templates.Add(template);
        await _context.SaveChangesAsync();
        _context.ChangeTracker.Clear();

        var all = await _repository.GetAllTemplatesAsync();

        var loaded = Assert.Single(all);
        // A cartesian-product single query would yield 3 x 2 x 4 = 24 joined rows; the
        // collections must still contain exactly the inserted counts.
        Assert.Equal(3, loaded.Attributes.Count);
        Assert.Equal(2, loaded.Alarms.Count);
        Assert.Equal(4, loaded.Scripts.Count);
    }

    [Fact]
    public async Task GetTemplateByIdAsync_WithMultipleMembers_LoadsAllCollections()
    {
        var template = new Template("Single");
        template.Attributes.Add(new TemplateAttribute("A1"));
        template.Attributes.Add(new TemplateAttribute("A2"));
        template.Scripts.Add(new TemplateScript("S1", "return 1;"));
        _context.Templates.Add(template);
        await _context.SaveChangesAsync();
        _context.ChangeTracker.Clear();

        var loaded = await _repository.GetTemplateByIdAsync(template.Id);

        Assert.NotNull(loaded);
        Assert.Equal(2, loaded!.Attributes.Count);
        Assert.Single(loaded.Scripts);
    }

    // Auth re-arch (C5): the SQL Server ApiKey entity was retired (inbound keys now
    // live in the shared ZB.MOM.WW.Auth.ApiKeys SQLite store), so the former
    // ApiKey_KeyHashColumn_IsMappedAndUniquelyIndexed and
    // ApiKey_HasNoPlaintextKeyValueColumn schema assertions were removed with it.
}
