using ClaudeDo.Ui.ViewModels.Modals;
using Xunit;

namespace ClaudeDo.Ui.Tests.ViewModels;

public class DiffAlignmentTests
{
    private static DiffLineViewModel Ctx(int oldNo, int newNo, string text) =>
        new() { Kind = DiffLineKind.Ctx, OldNo = oldNo, NewNo = newNo, Text = text };

    private static DiffLineViewModel Del(int oldNo, string text) =>
        new() { Kind = DiffLineKind.Del, OldNo = oldNo, Text = text };

    private static DiffLineViewModel Add(int newNo, string text) =>
        new() { Kind = DiffLineKind.Add, NewNo = newNo, Text = text };

    [Fact]
    public void NullOrEmpty_YieldsEmptyDiff()
    {
        Assert.Empty(DiffAlignment.Build(null).SplitRows);
        Assert.Empty(DiffAlignment.Build(Array.Empty<DiffLineViewModel>()).SplitRows);
    }

    [Fact]
    public void ContextOnly_MirrorsBothSides_WithNoFillers()
    {
        var result = DiffAlignment.Build(new[] { Ctx(1, 1, "a"), Ctx(2, 2, "b") });

        Assert.Equal(2, result.SplitRows.Count);
        Assert.All(result.SplitRows, r =>
        {
            Assert.Equal(AlignedSide.Ctx, r.LeftKind);
            Assert.Equal(AlignedSide.Ctx, r.RightKind);
            Assert.Equal(r.LeftText, r.RightText);
        });
        Assert.Equal("a\nb", result.LeftText);
        Assert.Equal("a\nb", result.RightText);
    }

    [Fact]
    public void EqualSizedChangeBlock_PairsOneToOne()
    {
        var result = DiffAlignment.Build(new[]
        {
            Del(1, "old one"), Del(2, "old two"),
            Add(1, "new one"), Add(2, "new two"),
        });

        Assert.Equal(2, result.SplitRows.Count);
        Assert.Equal(AlignedSide.Del, result.SplitRows[0].LeftKind);
        Assert.Equal(AlignedSide.Add, result.SplitRows[0].RightKind);
        Assert.Equal("old one", result.SplitRows[0].LeftText);
        Assert.Equal("new one", result.SplitRows[0].RightText);
        Assert.Equal(1, result.SplitRows[0].OldNo!.Value);
        Assert.Equal(1, result.SplitRows[0].NewNo!.Value);
    }

    [Fact]
    public void MoreAddsThanDels_PadsTheLeftSideWithFillers()
    {
        var result = DiffAlignment.Build(new[]
        {
            Del(1, "d1"),
            Add(1, "a1"), Add(2, "a2"), Add(3, "a3"),
        });

        Assert.Equal(3, result.SplitRows.Count);
        Assert.Equal(AlignedSide.Del, result.SplitRows[0].LeftKind);
        Assert.Equal(AlignedSide.Filler, result.SplitRows[1].LeftKind);
        Assert.Equal(AlignedSide.Filler, result.SplitRows[2].LeftKind);
        Assert.Equal("", result.SplitRows[1].LeftText);
        Assert.Null(result.SplitRows[1].OldNo);
        Assert.All(result.SplitRows, r => Assert.Equal(AlignedSide.Add, r.RightKind));
    }

    [Fact]
    public void MoreDelsThanAdds_PadsTheRightSideWithFillers()
    {
        var result = DiffAlignment.Build(new[]
        {
            Del(1, "d1"), Del(2, "d2"), Del(3, "d3"),
            Add(1, "a1"),
        });

        Assert.Equal(3, result.SplitRows.Count);
        Assert.Equal(AlignedSide.Add, result.SplitRows[0].RightKind);
        Assert.Equal(AlignedSide.Filler, result.SplitRows[1].RightKind);
        Assert.Equal(AlignedSide.Filler, result.SplitRows[2].RightKind);
        Assert.All(result.SplitRows, r => Assert.Equal(AlignedSide.Del, r.LeftKind));
    }

    [Fact]
    public void NonContiguousLineNumbers_InsertOneGapRow()
    {
        var result = DiffAlignment.Build(new[] { Ctx(1, 1, "a"), Ctx(40, 40, "b") });

        Assert.Equal(3, result.SplitRows.Count);
        Assert.Equal(AlignedSide.Gap, result.SplitRows[1].LeftKind);
        Assert.Equal(AlignedSide.Gap, result.SplitRows[1].RightKind);
        Assert.Null(result.SplitRows[1].OldNo);
        Assert.Null(result.SplitRows[1].NewNo);
    }

    [Fact]
    public void FirstLineNeverProducesALeadingGap()
    {
        var result = DiffAlignment.Build(new[] { Ctx(120, 118, "a") });

        Assert.Single(result.SplitRows);
        Assert.Equal(AlignedSide.Ctx, result.SplitRows[0].LeftKind);
    }

    [Fact]
    public void UnifiedRows_KeepGitOrder_DeletionsThenAdditions()
    {
        var result = DiffAlignment.Build(new[]
        {
            Ctx(1, 1, "keep"),
            Del(2, "d1"), Del(3, "d2"),
            Add(2, "a1"),
        });

        Assert.Equal(4, result.UnifiedRows.Count);
        Assert.Equal(AlignedSide.Ctx, result.UnifiedRows[0].Kind);
        Assert.Equal(AlignedSide.Del, result.UnifiedRows[1].Kind);
        Assert.Equal(AlignedSide.Del, result.UnifiedRows[2].Kind);
        Assert.Equal(AlignedSide.Add, result.UnifiedRows[3].Kind);
        Assert.Equal("keep\nd1\nd2\na1", result.UnifiedText);
    }

    [Fact]
    public void RowIndex_MapsToDocumentLine_OnBothSides()
    {
        var result = DiffAlignment.Build(new[]
        {
            Ctx(1, 1, "a"),
            Del(2, "d"), Add(2, "x"), Add(3, "y"),
            Ctx(3, 4, "b"),
        });

        var leftLines = result.LeftText.Split('\n');
        var rightLines = result.RightText.Split('\n');
        Assert.Equal(result.SplitRows.Count, leftLines.Length);
        Assert.Equal(result.SplitRows.Count, rightLines.Length);
        for (var i = 0; i < result.SplitRows.Count; i++)
        {
            Assert.Equal(result.SplitRows[i].LeftText, leftLines[i]);
            Assert.Equal(result.SplitRows[i].RightText, rightLines[i]);
        }

        var unifiedLines = result.UnifiedText.Split('\n');
        Assert.Equal(result.UnifiedRows.Count, unifiedLines.Length);
    }

    [Fact]
    public void FileHeaderRows_AreIgnored()
    {
        var result = DiffAlignment.Build(new[]
        {
            new DiffLineViewModel { Kind = DiffLineKind.File, Text = "src/Foo.cs" },
            Ctx(1, 1, "a"),
        });

        Assert.Single(result.SplitRows);
        Assert.Equal("a", result.SplitRows[0].LeftText);
    }

    [Fact]
    public void WordDiff_HighlightsOnlyTheChangedToken()
    {
        var result = DiffAlignment.Build(new[]
        {
            Del(1, "    var x = 1;"),
            Add(1, "    var x = 2;"),
        });

        var row = Assert.Single(result.SplitRows);
        var left = Assert.Single(row.LeftSpans);
        var right = Assert.Single(row.RightSpans);
        Assert.Equal(new TextSpan(12, 1), left);
        Assert.Equal(new TextSpan(12, 1), right);
        Assert.Equal("1", row.LeftText.Substring(left.Start, left.Length));
        Assert.Equal("2", row.RightText.Substring(right.Start, right.Length));
    }

    [Fact]
    public void WordDiff_MergesAdjacentChangedTokensIntoOneSpan()
    {
        var result = DiffAlignment.Build(new[]
        {
            Del(1, "call(a, b);"),
            Add(1, "call(zz, b);"),
        });

        var row = Assert.Single(result.SplitRows);
        var right = Assert.Single(row.RightSpans);
        Assert.Equal("zz", row.RightText.Substring(right.Start, right.Length));
    }

    [Fact]
    public void WordDiff_SkippedWhenTheLinesAreUnrelated()
    {
        var result = DiffAlignment.Build(new[]
        {
            Del(1, "public void Alpha()"),
            Add(1, "return 42;"),
        });

        var row = Assert.Single(result.SplitRows);
        Assert.Empty(row.LeftSpans);
        Assert.Empty(row.RightSpans);
    }

    [Fact]
    public void WordDiff_SkippedOnVeryLongLines()
    {
        var left = new string('a', DiffAlignment.MaxWordDiffChars + 1);
        var result = DiffAlignment.Build(new[] { Del(1, left), Add(1, left + "b") });

        var row = Assert.Single(result.SplitRows);
        Assert.Empty(row.LeftSpans);
        Assert.Empty(row.RightSpans);
    }

    [Fact]
    public void WordDiff_SkippedWhenTokenCountExceedsTheCap()
    {
        var many = string.Join(" ", Enumerable.Range(0, DiffAlignment.MaxWordDiffTokens + 10).Select(n => $"t{n}"));
        var result = DiffAlignment.Build(new[] { Del(1, many), Add(1, many + " x") });

        var row = Assert.Single(result.SplitRows);
        Assert.Empty(row.LeftSpans);
        Assert.Empty(row.RightSpans);
    }

    [Fact]
    public void WordDiff_IdenticalTextYieldsNoSpans()
    {
        var result = DiffAlignment.Build(new[] { Del(1, "same"), Add(1, "same") });

        var row = Assert.Single(result.SplitRows);
        Assert.Empty(row.LeftSpans);
        Assert.Empty(row.RightSpans);
    }

    [Fact]
    public void WordDiff_UnpairedOverhangRowsHaveNoSpans()
    {
        var result = DiffAlignment.Build(new[]
        {
            Del(1, "var x = 1;"),
            Add(1, "var x = 2;"), Add(2, "var y = 3;"),
        });

        Assert.NotEmpty(result.SplitRows[0].RightSpans);
        Assert.Empty(result.SplitRows[1].RightSpans);
    }
}
