using Moq;
using ScadaLink.Commons.Entities.Instances;
using ScadaLink.Commons.Entities.Scripts;
using ScadaLink.Commons.Entities.Templates;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Commons.Types.Enums;

namespace ScadaLink.TemplateEngine.Tests;

public class TemplateServiceTests
{
    private readonly Mock<ITemplateEngineRepository> _repoMock;
    private readonly Mock<IAuditService> _auditMock;
    private readonly TemplateService _service;

    public TemplateServiceTests()
    {
        _repoMock = new Mock<ITemplateEngineRepository>();
        _auditMock = new Mock<IAuditService>();
        _repoMock.Setup(r => r.SaveChangesAsync(It.IsAny<CancellationToken>())).ReturnsAsync(1);
        _service = new TemplateService(_repoMock.Object, _auditMock.Object);
    }

    // ========================================================================
    // WP-1: Template CRUD with Inheritance
    // ========================================================================

    [Fact]
    public async Task CreateTemplate_Success()
    {
        var result = await _service.CreateTemplateAsync("Pump", "A pump template", null, "admin");

        Assert.True(result.IsSuccess);
        Assert.Equal("Pump", result.Value.Name);
        _repoMock.Verify(r => r.AddTemplateAsync(It.IsAny<Template>(), It.IsAny<CancellationToken>()), Times.Once);
        _repoMock.Verify(r => r.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
        _auditMock.Verify(a => a.LogAsync("admin", "Create", "Template", It.IsAny<string>(), "Pump", It.IsAny<object?>(), It.IsAny<CancellationToken>()), Times.Once);
    }

    [Fact]
    public async Task CreateTemplate_EmptyName_Fails()
    {
        var result = await _service.CreateTemplateAsync("", null, null, "admin");
        Assert.True(result.IsFailure);
        Assert.Contains("required", result.Error);
    }

    [Fact]
    public async Task CreateTemplate_WithParent_Success()
    {
        var parent = new Template("Base") { Id = 1 };
        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(parent);
        _repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Template> { parent });

        var result = await _service.CreateTemplateAsync("Child", null, 1, "admin");

        Assert.True(result.IsSuccess);
        Assert.Equal(1, result.Value.ParentTemplateId);
    }

    [Fact]
    public async Task CreateTemplate_NonexistentParent_Fails()
    {
        _repoMock.Setup(r => r.GetTemplateByIdAsync(999, It.IsAny<CancellationToken>())).ReturnsAsync((Template?)null);

        var result = await _service.CreateTemplateAsync("Child", null, 999, "admin");
        Assert.True(result.IsFailure);
        Assert.Contains("not found", result.Error);
    }

    [Fact]
    public async Task DeleteTemplate_Success()
    {
        var template = new Template("Pump") { Id = 1 };
        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
        _repoMock.Setup(r => r.GetInstancesByTemplateIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Instance>());
        _repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Template> { template });

        var result = await _service.DeleteTemplateAsync(1, "admin");

        Assert.True(result.IsSuccess);
        _repoMock.Verify(r => r.DeleteTemplateAsync(1, It.IsAny<CancellationToken>()), Times.Once);
    }

    [Fact]
    public async Task DeleteTemplate_ReferencedByInstances_Fails()
    {
        var template = new Template("Pump") { Id = 1 };
        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
        _repoMock.Setup(r => r.GetInstancesByTemplateIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Instance> { new Instance("Pump1") { Id = 1, TemplateId = 1, SiteId = 1 } });

        var result = await _service.DeleteTemplateAsync(1, "admin");

        Assert.True(result.IsFailure);
        Assert.Contains("referenced by", result.Error);
    }

    [Fact]
    public async Task DeleteTemplate_HasChildren_Fails()
    {
        var parent = new Template("Base") { Id = 1 };
        var child = new Template("Child") { Id = 2, ParentTemplateId = 1 };
        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(parent);
        _repoMock.Setup(r => r.GetInstancesByTemplateIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Instance>());
        _repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Template> { parent, child });

        var result = await _service.DeleteTemplateAsync(1, "admin");

        Assert.True(result.IsFailure);
        Assert.Contains("inherited by", result.Error);
    }

    [Fact]
    public async Task DeleteTemplate_ComposedByOther_Fails()
    {
        var moduleTemplate = new Template("Module") { Id = 1 };
        var composingTemplate = new Template("Composing") { Id = 2 };
        composingTemplate.Compositions.Add(new TemplateComposition("mod1") { Id = 1, TemplateId = 2, ComposedTemplateId = 1 });

        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(moduleTemplate);
        _repoMock.Setup(r => r.GetInstancesByTemplateIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Instance>());
        _repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Template> { moduleTemplate, composingTemplate });

        var result = await _service.DeleteTemplateAsync(1, "admin");

        Assert.True(result.IsFailure);
        Assert.Contains("composed by", result.Error);
    }

    // ========================================================================
    // WP-2: Attribute Definitions with Lock Flags
    // ========================================================================

    [Fact]
    public async Task AddAttribute_Success()
    {
        var template = new Template("Pump") { Id = 1 };
        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
        _repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Template> { template });

        var attr = new TemplateAttribute("Temperature") { DataType = DataType.Float, Value = "0.0" };
        var result = await _service.AddAttributeAsync(1, attr, "admin");

        Assert.True(result.IsSuccess);
        Assert.Equal("Temperature", result.Value.Name);
        Assert.Equal(1, result.Value.TemplateId);
    }

    [Fact]
    public async Task AddAttribute_DuplicateName_Fails()
    {
        var template = new Template("Pump") { Id = 1 };
        template.Attributes.Add(new TemplateAttribute("Temperature") { Id = 1, TemplateId = 1, DataType = DataType.Float });
        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);

        var attr = new TemplateAttribute("Temperature") { DataType = DataType.Float };
        var result = await _service.AddAttributeAsync(1, attr, "admin");

        Assert.True(result.IsFailure);
        Assert.Contains("already exists", result.Error);
    }

    // ========================================================================
    // WP-3: Alarm Definitions
    // ========================================================================

    [Fact]
    public async Task AddAlarm_Success()
    {
        var template = new Template("Pump") { Id = 1 };
        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
        _repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Template> { template });

        var alarm = new TemplateAlarm("HighTemp")
        {
            PriorityLevel = 500,
            TriggerType = AlarmTriggerType.RangeViolation,
            TriggerConfiguration = """{"Max": 100}"""
        };
        var result = await _service.AddAlarmAsync(1, alarm, "admin");

        Assert.True(result.IsSuccess);
        Assert.Equal("HighTemp", result.Value.Name);
    }

    [Fact]
    public async Task AddAlarm_InvalidPriority_Fails()
    {
        var template = new Template("Pump") { Id = 1 };
        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);

        var alarm = new TemplateAlarm("HighTemp") { PriorityLevel = 1001, TriggerType = AlarmTriggerType.ValueMatch };
        var result = await _service.AddAlarmAsync(1, alarm, "admin");

        Assert.True(result.IsFailure);
        Assert.Contains("priority", result.Error, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public async Task UpdateAlarm_TriggerTypeFixed_Fails()
    {
        var existing = new TemplateAlarm("HighTemp")
        {
            Id = 1,
            TemplateId = 1,
            TriggerType = AlarmTriggerType.ValueMatch,
            PriorityLevel = 500
        };
        _repoMock.Setup(r => r.GetTemplateAlarmByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(existing);

        var template = new Template("Pump") { Id = 1 };
        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);

        var proposed = new TemplateAlarm("HighTemp")
        {
            TriggerType = AlarmTriggerType.RangeViolation, // Changed!
            PriorityLevel = 600
        };
        var result = await _service.UpdateAlarmAsync(1, proposed, "admin");

        Assert.True(result.IsFailure);
        Assert.Contains("TriggerType", result.Error);
    }

    // ========================================================================
    // WP-4: Script Definitions
    // ========================================================================

    [Fact]
    public async Task AddScript_Success()
    {
        var template = new Template("Pump") { Id = 1 };
        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
        _repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Template> { template });

        var script = new TemplateScript("OnStart", "return true;") { TriggerType = "Startup" };
        var result = await _service.AddScriptAsync(1, script, "admin");

        Assert.True(result.IsSuccess);
        Assert.Equal("OnStart", result.Value.Name);
    }

    [Fact]
    public async Task UpdateScript_NameFixed_Fails()
    {
        var existing = new TemplateScript("OnStart", "return true;") { Id = 1, TemplateId = 1 };
        _repoMock.Setup(r => r.GetTemplateScriptByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(existing);
        var template = new Template("Pump") { Id = 1 };
        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);

        var proposed = new TemplateScript("OnStop", "return false;"); // Name changed!
        var result = await _service.UpdateScriptAsync(1, proposed, "admin");

        Assert.True(result.IsFailure);
        Assert.Contains("Name", result.Error);
    }

    // ========================================================================
    // WP-5: Shared Script CRUD (see SharedScriptServiceTests)
    // ========================================================================

    // ========================================================================
    // WP-6: Composition with Recursive Nesting
    // ========================================================================

    [Fact]
    public async Task AddComposition_Success()
    {
        var moduleTemplate = new Template("Module") { Id = 2 };
        var template = new Template("Parent") { Id = 1 };

        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
        _repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(moduleTemplate);
        _repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Template> { template, moduleTemplate });

        var result = await _service.AddCompositionAsync(1, 2, "myModule", "admin");

        Assert.True(result.IsSuccess);
        Assert.Equal("myModule", result.Value.InstanceName);
        Assert.Equal(2, result.Value.ComposedTemplateId);
    }

    [Fact]
    public async Task AddComposition_DuplicateInstanceName_Fails()
    {
        var moduleTemplate = new Template("Module") { Id = 2 };
        var template = new Template("Parent") { Id = 1 };
        template.Compositions.Add(new TemplateComposition("myModule") { Id = 1, TemplateId = 1, ComposedTemplateId = 2 });

        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
        _repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(moduleTemplate);

        var result = await _service.AddCompositionAsync(1, 2, "myModule", "admin");

        Assert.True(result.IsFailure);
        Assert.Contains("already exists", result.Error);
    }

    [Fact]
    public async Task AddComposition_SelfComposition_Fails()
    {
        var template = new Template("Self") { Id = 1 };
        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
        _repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Template> { template });

        var result = await _service.AddCompositionAsync(1, 1, "self", "admin");

        Assert.True(result.IsFailure);
        Assert.Contains("compose itself", result.Error);
    }

    // ========================================================================
    // WP-9: Locking Rules
    // ========================================================================

    [Fact]
    public async Task UpdateAttribute_LockedMember_CannotUnlock()
    {
        var existing = new TemplateAttribute("Temperature")
        {
            Id = 1, TemplateId = 1, DataType = DataType.Float, IsLocked = true
        };
        _repoMock.Setup(r => r.GetTemplateAttributeByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(existing);
        var template = new Template("Pump") { Id = 1 };
        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);

        var proposed = new TemplateAttribute("Temperature")
        {
            DataType = DataType.Float, IsLocked = false, Value = "42"
        };
        var result = await _service.UpdateAttributeAsync(1, proposed, "admin");

        Assert.True(result.IsFailure);
        Assert.Contains("cannot be unlocked", result.Error);
    }

    [Fact]
    public async Task UpdateAttribute_LockUnlockedMember_Succeeds()
    {
        var existing = new TemplateAttribute("Temperature")
        {
            Id = 1, TemplateId = 1, DataType = DataType.Float, IsLocked = false
        };
        _repoMock.Setup(r => r.GetTemplateAttributeByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(existing);
        var template = new Template("Pump") { Id = 1 };
        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);

        var proposed = new TemplateAttribute("Temperature")
        {
            DataType = DataType.Float, IsLocked = true, Value = "42"
        };
        var result = await _service.UpdateAttributeAsync(1, proposed, "admin");

        Assert.True(result.IsSuccess);
        Assert.True(result.Value.IsLocked);
    }

    [Fact]
    public async Task UpdateAttribute_ParentLocked_CannotOverride()
    {
        // Parent template with locked attribute
        var parentTemplate = new Template("Base") { Id = 1 };
        parentTemplate.Attributes.Add(new TemplateAttribute("Speed")
        {
            Id = 10, TemplateId = 1, DataType = DataType.Float, IsLocked = true
        });

        // Child template overriding same attribute
        var childTemplate = new Template("Child") { Id = 2, ParentTemplateId = 1 };
        var childAttr = new TemplateAttribute("Speed")
        {
            Id = 20, TemplateId = 2, DataType = DataType.Float, IsLocked = false
        };
        childTemplate.Attributes.Add(childAttr);

        _repoMock.Setup(r => r.GetTemplateAttributeByIdAsync(20, It.IsAny<CancellationToken>())).ReturnsAsync(childAttr);
        _repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(childTemplate);
        _repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Template> { parentTemplate, childTemplate });

        var proposed = new TemplateAttribute("Speed")
        {
            DataType = DataType.Float, IsLocked = false, Value = "100"
        };
        var result = await _service.UpdateAttributeAsync(20, proposed, "admin");

        Assert.True(result.IsFailure);
        Assert.Contains("locked in parent", result.Error);
    }

    // ========================================================================
    // WP-10: Inheritance Override Scope — Cannot remove parent members
    // ========================================================================

    [Fact]
    public async Task DeleteAttribute_InheritedFromParent_Fails()
    {
        var parentTemplate = new Template("Base") { Id = 1 };
        parentTemplate.Attributes.Add(new TemplateAttribute("Speed")
        {
            Id = 10, TemplateId = 1, DataType = DataType.Float
        });

        var childTemplate = new Template("Child") { Id = 2, ParentTemplateId = 1 };
        var childAttr = new TemplateAttribute("Speed")
        {
            Id = 20, TemplateId = 2, DataType = DataType.Float
        };
        childTemplate.Attributes.Add(childAttr);

        _repoMock.Setup(r => r.GetTemplateAttributeByIdAsync(20, It.IsAny<CancellationToken>())).ReturnsAsync(childAttr);
        _repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(childTemplate);
        _repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Template> { parentTemplate, childTemplate });

        var result = await _service.DeleteAttributeAsync(20, "admin");

        Assert.True(result.IsFailure);
        Assert.Contains("inherited from parent", result.Error);
    }

    [Fact]
    public async Task DeleteAttribute_OwnMember_Succeeds()
    {
        var template = new Template("Pump") { Id = 1 };
        var attr = new TemplateAttribute("CustomAttr")
        {
            Id = 1, TemplateId = 1, DataType = DataType.String
        };
        template.Attributes.Add(attr);

        _repoMock.Setup(r => r.GetTemplateAttributeByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(attr);
        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);

        var result = await _service.DeleteAttributeAsync(1, "admin");

        Assert.True(result.IsSuccess);
    }

    // ========================================================================
    // WP-13: Graph Acyclicity
    // ========================================================================

    [Fact]
    public async Task UpdateTemplate_InheritanceCycle_Fails()
    {
        var templateA = new Template("A") { Id = 1, ParentTemplateId = null };
        var templateB = new Template("B") { Id = 2, ParentTemplateId = 1 };

        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(templateA);
        _repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(templateB);
        _repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Template> { templateA, templateB });

        // Try to make A inherit from B (B already inherits from A) => cycle
        var result = await _service.UpdateTemplateAsync(1, "A", null, 2, "admin");

        Assert.True(result.IsFailure);
        Assert.Contains("cycle", result.Error, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public async Task UpdateTemplate_SelfInheritance_Fails()
    {
        var template = new Template("Self") { Id = 1 };
        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
        _repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Template> { template });

        var result = await _service.UpdateTemplateAsync(1, "Self", null, 1, "admin");

        Assert.True(result.IsFailure);
        Assert.Contains("itself", result.Error, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public async Task AddComposition_CircularChain_Fails()
    {
        // A composes B, B composes C, try to make C compose A => cycle
        var templateC = new Template("C") { Id = 3 };
        var templateB = new Template("B") { Id = 2 };
        templateB.Compositions.Add(new TemplateComposition("c1") { Id = 2, TemplateId = 2, ComposedTemplateId = 3 });
        var templateA = new Template("A") { Id = 1 };
        templateA.Compositions.Add(new TemplateComposition("b1") { Id = 1, TemplateId = 1, ComposedTemplateId = 2 });

        _repoMock.Setup(r => r.GetTemplateByIdAsync(3, It.IsAny<CancellationToken>())).ReturnsAsync(templateC);
        _repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(templateA);
        _repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Template> { templateA, templateB, templateC });

        var result = await _service.AddCompositionAsync(3, 1, "a1", "admin");

        Assert.True(result.IsFailure);
        Assert.Contains("cycle", result.Error, StringComparison.OrdinalIgnoreCase);
    }
}
