using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Entities.Deployment;
using ScadaLink.Commons.Entities.ExternalSystems;
using ScadaLink.Commons.Entities.InboundApi;
using ScadaLink.Commons.Entities.Instances;
using ScadaLink.Commons.Entities.Notifications;
using ScadaLink.Commons.Entities.Scripts;
using ScadaLink.Commons.Entities.Security;
using ScadaLink.Commons.Entities.Sites;
using ScadaLink.Commons.Entities.Templates;
using ScadaLink.Commons.Types.Enums;
using ScadaLink.ConfigurationDatabase;

namespace ScadaLink.ConfigurationDatabase.Tests;

public class DbContextTests : IDisposable
{
    private readonly ScadaLinkDbContext _context;

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

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

    [Fact]
    public void Schema_CreatesAllTables()
    {
        // Verify all DbSet tables exist by checking we can query them without error
        Assert.NotNull(_context.Templates);
        Assert.NotNull(_context.TemplateAttributes);
        Assert.NotNull(_context.TemplateAlarms);
        Assert.NotNull(_context.TemplateScripts);
        Assert.NotNull(_context.TemplateCompositions);
        Assert.NotNull(_context.Instances);
        Assert.NotNull(_context.InstanceAttributeOverrides);
        Assert.NotNull(_context.InstanceConnectionBindings);
        Assert.NotNull(_context.Areas);
        Assert.NotNull(_context.Sites);
        Assert.NotNull(_context.DataConnections);
        Assert.NotNull(_context.DeploymentRecords);
        Assert.NotNull(_context.SystemArtifactDeploymentRecords);
        Assert.NotNull(_context.ExternalSystemDefinitions);
        Assert.NotNull(_context.ExternalSystemMethods);
        Assert.NotNull(_context.DatabaseConnectionDefinitions);
        Assert.NotNull(_context.NotificationLists);
        Assert.NotNull(_context.NotificationRecipients);
        Assert.NotNull(_context.SmtpConfigurations);
        Assert.NotNull(_context.SharedScripts);
        Assert.NotNull(_context.LdapGroupMappings);
        Assert.NotNull(_context.SiteScopeRules);
        Assert.NotNull(_context.ApiKeys);
        Assert.NotNull(_context.ApiMethods);
        Assert.NotNull(_context.AuditLogEntries);

        // Verify we can enumerate all tables (schema is valid)
        Assert.Empty(_context.Templates.ToList());
        Assert.Empty(_context.Sites.ToList());
        Assert.Empty(_context.Instances.ToList());
        Assert.Empty(_context.AuditLogEntries.ToList());
    }

    [Fact]
    public void Template_WithChildren_CascadeCreated()
    {
        var template = new Template("TestTemplate")
        {
            Description = "A test template"
        };
        template.Attributes.Add(new TemplateAttribute("Attr1") { DataType = DataType.Int32 });
        template.Alarms.Add(new TemplateAlarm("Alarm1") { TriggerType = AlarmTriggerType.ValueMatch, PriorityLevel = 1 });
        template.Scripts.Add(new TemplateScript("Script1", "return 42;"));

        _context.Templates.Add(template);
        _context.SaveChanges();

        var loaded = _context.Templates
            .Include(t => t.Attributes)
            .Include(t => t.Alarms)
            .Include(t => t.Scripts)
            .Single(t => t.Name == "TestTemplate");

        Assert.Single(loaded.Attributes);
        Assert.Single(loaded.Alarms);
        Assert.Single(loaded.Scripts);
        Assert.Equal("Attr1", loaded.Attributes.First().Name);
    }

    [Fact]
    public void Template_Inheritance_SelfReference()
    {
        var parent = new Template("ParentTemplate");
        _context.Templates.Add(parent);
        _context.SaveChanges();

        var child = new Template("ChildTemplate") { ParentTemplateId = parent.Id };
        _context.Templates.Add(child);
        _context.SaveChanges();

        var loaded = _context.Templates.Single(t => t.Name == "ChildTemplate");
        Assert.Equal(parent.Id, loaded.ParentTemplateId);
    }

    [Fact]
    public void Template_Composition_CreatesRelationship()
    {
        var composedTemplate = new Template("ComposedTemplate");
        var parentTemplate = new Template("ParentTemplate");
        _context.Templates.AddRange(composedTemplate, parentTemplate);
        _context.SaveChanges();

        parentTemplate.Compositions.Add(new TemplateComposition("Module1") { ComposedTemplateId = composedTemplate.Id });
        _context.SaveChanges();

        var loaded = _context.Templates
            .Include(t => t.Compositions)
            .Single(t => t.Name == "ParentTemplate");

        Assert.Single(loaded.Compositions);
        Assert.Equal(composedTemplate.Id, loaded.Compositions.First().ComposedTemplateId);
    }

    [Fact]
    public void Instance_WithOverridesAndBindings()
    {
        var site = new Site("Site1", "SITE-001");
        var template = new Template("Template1");
        _context.Sites.Add(site);
        _context.Templates.Add(template);
        _context.SaveChanges();

        var dataConn = new DataConnection("OpcConn", "OpcUa", site.Id);
        _context.DataConnections.Add(dataConn);
        _context.SaveChanges();

        var instance = new Instance("Instance1")
        {
            TemplateId = template.Id,
            SiteId = site.Id,
            State = InstanceState.Enabled
        };
        instance.AttributeOverrides.Add(new InstanceAttributeOverride("Attr1") { OverrideValue = "42" });
        instance.ConnectionBindings.Add(new InstanceConnectionBinding("TagPath") { DataConnectionId = dataConn.Id });
        _context.Instances.Add(instance);
        _context.SaveChanges();

        var loaded = _context.Instances
            .Include(i => i.AttributeOverrides)
            .Include(i => i.ConnectionBindings)
            .Single(i => i.UniqueName == "Instance1");

        Assert.Single(loaded.AttributeOverrides);
        Assert.Single(loaded.ConnectionBindings);
        Assert.Equal(InstanceState.Enabled, loaded.State);
    }

    [Fact]
    public void DeploymentRecord_CreatesWithAllFields()
    {
        var site = new Site("Site1", "SITE-001");
        var template = new Template("Template1");
        _context.Sites.Add(site);
        _context.Templates.Add(template);
        _context.SaveChanges();

        var instance = new Instance("Instance1") { TemplateId = template.Id, SiteId = site.Id, State = InstanceState.Enabled };
        _context.Instances.Add(instance);
        _context.SaveChanges();

        var record = new DeploymentRecord("deploy-001", "admin")
        {
            InstanceId = instance.Id,
            Status = DeploymentStatus.Success,
            RevisionHash = "abc123",
            DeployedAt = DateTimeOffset.UtcNow,
            CompletedAt = DateTimeOffset.UtcNow
        };
        _context.DeploymentRecords.Add(record);
        _context.SaveChanges();

        var loaded = _context.DeploymentRecords.Single(d => d.DeploymentId == "deploy-001");
        Assert.Equal(DeploymentStatus.Success, loaded.Status);
        Assert.Equal("abc123", loaded.RevisionHash);
    }

    [Fact]
    public void AuditLogEntry_WritesAndQueries()
    {
        var entry = new AuditLogEntry("admin", "Create", "Template", "1", "TestTemplate")
        {
            Timestamp = DateTimeOffset.UtcNow,
            AfterStateJson = "{\"name\":\"TestTemplate\"}"
        };
        _context.AuditLogEntries.Add(entry);
        _context.SaveChanges();

        var loaded = _context.AuditLogEntries.Single(a => a.User == "admin");
        Assert.Equal("Create", loaded.Action);
        Assert.Equal("Template", loaded.EntityType);
        Assert.NotNull(loaded.AfterStateJson);
    }

    [Fact]
    public void ExternalSystem_WithMethods()
    {
        var system = new ExternalSystemDefinition("ERP", "https://erp.example.com/api", "ApiKey")
        {
            MaxRetries = 3,
            RetryDelay = TimeSpan.FromSeconds(5)
        };
        _context.ExternalSystemDefinitions.Add(system);
        _context.SaveChanges();

        var method = new ExternalSystemMethod("GetOrder", "GET", "/orders/{id}")
        {
            ExternalSystemDefinitionId = system.Id
        };
        _context.ExternalSystemMethods.Add(method);
        _context.SaveChanges();

        Assert.Single(_context.ExternalSystemMethods.Where(m => m.ExternalSystemDefinitionId == system.Id));
    }

    [Fact]
    public void NotificationList_WithRecipients()
    {
        var list = new NotificationList("Operators");
        list.Recipients.Add(new NotificationRecipient("John", "john@example.com"));
        list.Recipients.Add(new NotificationRecipient("Jane", "jane@example.com"));

        _context.NotificationLists.Add(list);
        _context.SaveChanges();

        var loaded = _context.NotificationLists
            .Include(n => n.Recipients)
            .Single(n => n.Name == "Operators");

        Assert.Equal(2, loaded.Recipients.Count);
    }

    [Fact]
    public void Security_LdapGroupMapping_WithSiteScopeRules()
    {
        var site = new Site("Site1", "SITE-001");
        _context.Sites.Add(site);
        _context.SaveChanges();

        var mapping = new LdapGroupMapping("CN=Admins,DC=example,DC=com", "Admin");
        _context.LdapGroupMappings.Add(mapping);
        _context.SaveChanges();

        var rule = new SiteScopeRule { LdapGroupMappingId = mapping.Id, SiteId = site.Id };
        _context.SiteScopeRules.Add(rule);
        _context.SaveChanges();

        Assert.Single(_context.SiteScopeRules.Where(r => r.LdapGroupMappingId == mapping.Id));
    }

    [Fact]
    public void InboundApi_ApiKeyAndMethod()
    {
        var key = new ApiKey("TestKey", "sk-test-123") { IsEnabled = true };
        var method = new ApiMethod("GetStatus", "return \"ok\";") { TimeoutSeconds = 30 };

        _context.ApiKeys.Add(key);
        _context.ApiMethods.Add(method);
        _context.SaveChanges();

        Assert.Single(_context.ApiKeys.Where(k => k.Name == "TestKey"));
        Assert.Single(_context.ApiMethods.Where(m => m.Name == "GetStatus"));
    }

    [Fact]
    public void Area_HierarchyWorks()
    {
        var site = new Site("Site1", "SITE-001");
        _context.Sites.Add(site);
        _context.SaveChanges();

        var parentArea = new Area("Building A") { SiteId = site.Id };
        _context.Areas.Add(parentArea);
        _context.SaveChanges();

        var childArea = new Area("Floor 1") { SiteId = site.Id, ParentAreaId = parentArea.Id };
        _context.Areas.Add(childArea);
        _context.SaveChanges();

        var loaded = _context.Areas
            .Include(a => a.Children)
            .Single(a => a.Name == "Building A");

        Assert.Single(loaded.Children);
        Assert.Equal("Floor 1", loaded.Children.First().Name);
    }

    [Fact]
    public void DataConnection_BelongsToSite()
    {
        var site = new Site("Site1", "SITE-001");
        _context.Sites.Add(site);
        _context.SaveChanges();

        var conn = new DataConnection("OpcConn", "OpcUa", site.Id);
        _context.DataConnections.Add(conn);
        _context.SaveChanges();

        var loaded = _context.DataConnections.Single(c => c.Name == "OpcConn");
        Assert.Equal(site.Id, loaded.SiteId);
    }

    [Fact]
    public void EnumProperties_StoredAsStrings()
    {
        var template = new Template("EnumTest");
        template.Attributes.Add(new TemplateAttribute("Attr1") { DataType = DataType.Double });
        template.Alarms.Add(new TemplateAlarm("Alarm1") { TriggerType = AlarmTriggerType.RangeViolation, PriorityLevel = 1 });
        _context.Templates.Add(template);
        _context.SaveChanges();

        // Query using raw SQL to verify string storage
        var attr = _context.TemplateAttributes.Single(a => a.Name == "Attr1");
        Assert.Equal(DataType.Double, attr.DataType);

        var alarm = _context.TemplateAlarms.Single(a => a.Name == "Alarm1");
        Assert.Equal(AlarmTriggerType.RangeViolation, alarm.TriggerType);
    }

    [Fact]
    public void UniqueConstraint_Template_Name_Enforced()
    {
        _context.Templates.Add(new Template("Unique"));
        _context.SaveChanges();

        _context.Templates.Add(new Template("Unique"));
        Assert.ThrowsAny<Exception>(() => _context.SaveChanges());
    }

    [Fact]
    public void DateTimeOffset_MappedCorrectly()
    {
        var now = DateTimeOffset.UtcNow;
        var entry = new AuditLogEntry("user", "Test", "Entity", "1", "Name") { Timestamp = now };
        _context.AuditLogEntries.Add(entry);
        _context.SaveChanges();

        _context.ChangeTracker.Clear();
        var loaded = _context.AuditLogEntries.Single();
        // SQLite has limited DateTimeOffset precision, but the round-trip should preserve the value within a second
        Assert.True(Math.Abs((loaded.Timestamp - now).TotalSeconds) < 1);
    }
}

public class ServiceRegistrationTests
{
    [Fact]
    public void AddConfigurationDatabase_WithConnectionString_RegistersDbContext()
    {
        var services = new ServiceCollection();
        services.AddConfigurationDatabase("DataSource=:memory:");

        var provider = services.BuildServiceProvider();
        var context = provider.GetService<ScadaLinkDbContext>();

        Assert.NotNull(context);
    }

    [Fact]
    public void AddConfigurationDatabase_NoArgs_FailsFast()
    {
        // ConfigurationDatabase-003: the no-arg overload previously silently registered
        // nothing, which deferred a misconfiguration into an opaque DI failure later.
        // It is now [Obsolete(error: true)] (compile-time guard) and throws at runtime.
        // Invoked via reflection because the obsolete-error overload cannot be called
        // directly from source.
        var method = typeof(ServiceCollectionExtensions).GetMethod(
            nameof(ServiceCollectionExtensions.AddConfigurationDatabase),
            System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static,
            binder: null,
            types: new[] { typeof(IServiceCollection) },
            modifiers: null)!;
        var services = new ServiceCollection();

        var invocation = Assert.Throws<System.Reflection.TargetInvocationException>(
            () => method.Invoke(null, new object[] { services }));
        Assert.IsType<InvalidOperationException>(invocation.InnerException);
    }
}

public class MigrationHelperTests : IDisposable
{
    private readonly ScadaLinkDbContext _context;

    public MigrationHelperTests()
    {
        // Use SQLite with PendingModelChangesWarning suppressed because the migration
        // was generated for SQL Server and SQLite's model representation differs slightly.
        var options = new DbContextOptionsBuilder<ScadaLinkDbContext>()
            .UseSqlite("DataSource=:memory:")
            .ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning))
            .Options;

        _context = new ScadaLinkDbContext(options);
        _context.Database.OpenConnection();
    }

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

    [Fact]
    public async Task ApplyOrValidate_ProductionMode_WithPendingMigrations_Throws()
    {
        // Database has no schema yet, so pending migrations exist.
        // The production path uses GetPendingMigrationsAsync which works cross-provider.
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => MigrationHelper.ApplyOrValidateMigrationsAsync(_context, isDevelopment: false));

        Assert.Contains("pending migration", ex.Message, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void MigrationExists_InitialCreate()
    {
        // Verify the InitialCreate migration is detected as pending
        var pending = _context.Database.GetPendingMigrations().ToList();
        Assert.Contains(pending, m => m.Contains("InitialSchema"));
    }
}
