using Moq;
using ScadaLink.Commons.Entities.Instances;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.TemplateEngine.Services;

namespace ScadaLink.TemplateEngine.Tests.Services;

public class AreaServiceTests
{
    private readonly Mock<ITemplateEngineRepository> _repoMock = new();
    private readonly Mock<IAuditService> _auditMock = new();
    private readonly AreaService _sut;

    public AreaServiceTests()
    {
        _sut = new AreaService(_repoMock.Object, _auditMock.Object);
    }

    [Fact]
    public async Task CreateArea_ValidInput_ReturnsSuccess()
    {
        _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Area>());
        _repoMock.Setup(r => r.SaveChangesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(1);

        var result = await _sut.CreateAreaAsync("Building A", 1, null, "admin");

        Assert.True(result.IsSuccess);
        Assert.Equal("Building A", result.Value.Name);
        Assert.Equal(1, result.Value.SiteId);
    }

    [Fact]
    public async Task CreateArea_DuplicateName_ReturnsFailure()
    {
        _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Area>
            {
                new("Building A") { Id = 1, SiteId = 1, ParentAreaId = null }
            });

        var result = await _sut.CreateAreaAsync("Building A", 1, null, "admin");

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

    [Fact]
    public async Task CreateArea_WithParent_ValidatesParentBelongsToSite()
    {
        _repoMock.Setup(r => r.GetAreaByIdAsync(5, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new Area("Parent") { Id = 5, SiteId = 99 }); // Different site!

        var result = await _sut.CreateAreaAsync("Child", 1, 5, "admin");

        Assert.True(result.IsFailure);
        Assert.Contains("does not belong", result.Error);
    }

    [Fact]
    public async Task DeleteArea_WithAssignedInstances_ReturnsFailure()
    {
        _repoMock.Setup(r => r.GetAreaByIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new Area("Building A") { Id = 1, SiteId = 1 });
        _repoMock.Setup(r => r.GetInstancesBySiteIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Instance>
            {
                new("Inst1") { Id = 1, AreaId = 1, SiteId = 1 }
            });
        _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Area> { new("Building A") { Id = 1, SiteId = 1 } });

        var result = await _sut.DeleteAreaAsync(1, "admin");

        Assert.True(result.IsFailure);
        Assert.Contains("instance(s) are assigned", result.Error);
    }

    [Fact]
    public async Task DeleteArea_WithChildAreas_ReturnsFailure()
    {
        _repoMock.Setup(r => r.GetAreaByIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new Area("Parent") { Id = 1, SiteId = 1 });
        _repoMock.Setup(r => r.GetInstancesBySiteIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Instance>());
        _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Area>
            {
                new("Parent") { Id = 1, SiteId = 1 },
                new("Child") { Id = 2, SiteId = 1, ParentAreaId = 1 }
            });

        var result = await _sut.DeleteAreaAsync(1, "admin");

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

    [Fact]
    public async Task DeleteArea_NoConstraints_Success()
    {
        _repoMock.Setup(r => r.GetAreaByIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new Area("Empty") { Id = 1, SiteId = 1 });
        _repoMock.Setup(r => r.GetInstancesBySiteIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Instance>());
        _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Area> { new("Empty") { Id = 1, SiteId = 1 } });
        _repoMock.Setup(r => r.SaveChangesAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(1);

        var result = await _sut.DeleteAreaAsync(1, "admin");

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

    [Fact]
    public async Task MoveArea_ToOtherArea_Succeeds()
    {
        // Move 'Leaf' from under 'A' to under 'B'.
        _repoMock.Setup(r => r.GetAreaByIdAsync(3, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new Area("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 1 });
        _repoMock.Setup(r => r.GetAreaByIdAsync(2, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new Area("B") { Id = 2, SiteId = 1 });
        _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Area>
            {
                new("A") { Id = 1, SiteId = 1 },
                new("B") { Id = 2, SiteId = 1 },
                new("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 1 }
            });
        _repoMock.Setup(r => r.SaveChangesAsync(It.IsAny<CancellationToken>())).ReturnsAsync(1);

        var result = await _sut.MoveAreaAsync(3, 2, "admin");

        Assert.True(result.IsSuccess);
        Assert.Equal(2, result.Value.ParentAreaId);
        _repoMock.Verify(r => r.UpdateAreaAsync(It.Is<Area>(a => a.Id == 3 && a.ParentAreaId == 2), It.IsAny<CancellationToken>()), Times.Once);
        _auditMock.Verify(a => a.LogAsync("admin", "Move", "Area", "3", "Leaf", It.IsAny<object>(), It.IsAny<CancellationToken>()), Times.Once);
    }

    [Fact]
    public async Task MoveArea_ToSiteRoot_Succeeds()
    {
        // Move 'Leaf' from under 'A' to site root (null parent).
        _repoMock.Setup(r => r.GetAreaByIdAsync(3, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new Area("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 1 });
        _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Area>
            {
                new("A") { Id = 1, SiteId = 1 },
                new("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 1 }
            });
        _repoMock.Setup(r => r.SaveChangesAsync(It.IsAny<CancellationToken>())).ReturnsAsync(1);

        var result = await _sut.MoveAreaAsync(3, null, "admin");

        Assert.True(result.IsSuccess);
        Assert.Null(result.Value.ParentAreaId);
    }

    [Fact]
    public async Task MoveArea_ToSelf_Fails()
    {
        _repoMock.Setup(r => r.GetAreaByIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new Area("A") { Id = 1, SiteId = 1 });

        var result = await _sut.MoveAreaAsync(1, 1, "admin");

        Assert.True(result.IsFailure);
        Assert.Contains("its own parent", result.Error);
    }

    [Fact]
    public async Task MoveArea_ToDescendant_FailsWithCycleError()
    {
        // Tree: 1 -> 2 -> 3. Try to move 1 under 3.
        _repoMock.Setup(r => r.GetAreaByIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new Area("Root") { Id = 1, SiteId = 1 });
        _repoMock.Setup(r => r.GetAreaByIdAsync(3, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new Area("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 2 });
        _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Area>
            {
                new("Root") { Id = 1, SiteId = 1 },
                new("Mid") { Id = 2, SiteId = 1, ParentAreaId = 1 },
                new("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 2 }
            });

        var result = await _sut.MoveAreaAsync(1, 3, "admin");

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

    [Fact]
    public async Task MoveArea_DifferentSite_Fails()
    {
        _repoMock.Setup(r => r.GetAreaByIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new Area("A") { Id = 1, SiteId = 1 });
        _repoMock.Setup(r => r.GetAreaByIdAsync(99, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new Area("Foreign") { Id = 99, SiteId = 2 });

        var result = await _sut.MoveAreaAsync(1, 99, "admin");

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

    [Fact]
    public async Task MoveArea_NameCollidesAtNewParent_Fails()
    {
        // 'Leaf' under parent 1; a sibling 'Leaf' already exists under parent 2.
        _repoMock.Setup(r => r.GetAreaByIdAsync(3, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new Area("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 1 });
        _repoMock.Setup(r => r.GetAreaByIdAsync(2, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new Area("B") { Id = 2, SiteId = 1 });
        _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Area>
            {
                new("A") { Id = 1, SiteId = 1 },
                new("B") { Id = 2, SiteId = 1 },
                new("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 1 },
                new("Leaf") { Id = 4, SiteId = 1, ParentAreaId = 2 }
            });

        var result = await _sut.MoveAreaAsync(3, 2, "admin");

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

    [Fact]
    public async Task MoveArea_SameParent_NoOpSuccess()
    {
        _repoMock.Setup(r => r.GetAreaByIdAsync(3, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new Area("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 1 });
        _repoMock.Setup(r => r.GetAreaByIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new Area("A") { Id = 1, SiteId = 1 });
        _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Area>
            {
                new("A") { Id = 1, SiteId = 1 },
                new("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 1 }
            });

        var result = await _sut.MoveAreaAsync(3, 1, "admin");

        Assert.True(result.IsSuccess);
        _repoMock.Verify(r => r.UpdateAreaAsync(It.IsAny<Area>(), It.IsAny<CancellationToken>()), Times.Never);
        _auditMock.Verify(a => a.LogAsync(It.IsAny<string>(), "Move", It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<object>(), It.IsAny<CancellationToken>()), Times.Never);
    }

    [Fact]
    public async Task MoveArea_TargetParentMissing_Fails()
    {
        _repoMock.Setup(r => r.GetAreaByIdAsync(3, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new Area("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 1 });
        _repoMock.Setup(r => r.GetAreaByIdAsync(999, It.IsAny<CancellationToken>()))
            .ReturnsAsync((Area?)null);

        var result = await _sut.MoveAreaAsync(3, 999, "admin");

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

    [Fact]
    public async Task DeleteArea_InstancesInDescendants_Blocked()
    {
        // Area hierarchy: Area1 -> Area2 -> Area3
        // Instance assigned to Area3, trying to delete Area1 should be blocked
        _repoMock.Setup(r => r.GetAreaByIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new Area("Root") { Id = 1, SiteId = 1 });
        _repoMock.Setup(r => r.GetAreasBySiteIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Area>
            {
                new("Root") { Id = 1, SiteId = 1 },
                new("Mid") { Id = 2, SiteId = 1, ParentAreaId = 1 },
                new("Leaf") { Id = 3, SiteId = 1, ParentAreaId = 2 }
            });
        _repoMock.Setup(r => r.GetInstancesBySiteIdAsync(1, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Instance>
            {
                new("DeepInstance") { Id = 10, AreaId = 3, SiteId = 1 }
            });

        var result = await _sut.DeleteAreaAsync(1, "admin");

        Assert.True(result.IsFailure);
        Assert.Contains("instance(s) are assigned", result.Error);
    }
}
