using System.Diagnostics;

using ZB.MOM.WW.ScadaBridge.Transport.Import;

namespace ZB.MOM.WW.ScadaBridge.Transport.Tests.Import;

public sealed class LineDifferTests
{
    [Fact]
    public void IdenticalText_NoAddsOrRemoves_NotTruncated()
    {
        var result = LineDiffer.Diff("a\nb\nc", "a\nb\nc");

        Assert.Equal(0, result.AddedCount);
        Assert.Equal(0, result.RemovedCount);
        Assert.False(result.Truncated);
        // All emitted lines (if any) must be Context.
        Assert.All(result.Lines, l => Assert.Equal(LineDiffOp.Context, l.Op));
    }

    [Fact]
    public void OldEmpty_NewThreeLines_ThreeAdds_NewLineNos1To3()
    {
        var result = LineDiffer.Diff("", "x\ny\nz");

        var adds = result.Lines.Where(l => l.Op == LineDiffOp.Add).ToList();
        Assert.Equal(3, result.AddedCount);
        Assert.Equal(0, result.RemovedCount);
        Assert.False(result.Truncated);
        Assert.Equal(3, adds.Count);
        Assert.Equal(new[] { 1, 2, 3 }, adds.Select(a => a.NewLineNo!.Value).ToArray());
        Assert.Equal(new[] { "x", "y", "z" }, adds.Select(a => a.Text).ToArray());
        // Add lines have no old line number.
        Assert.All(adds, a => Assert.Null(a.OldLineNo));
    }

    [Fact]
    public void NewEmpty_OldTwoLines_TwoRemoves_OldLineNos1To2()
    {
        var result = LineDiffer.Diff("p\nq", "");

        var removes = result.Lines.Where(l => l.Op == LineDiffOp.Remove).ToList();
        Assert.Equal(0, result.AddedCount);
        Assert.Equal(2, result.RemovedCount);
        Assert.False(result.Truncated);
        Assert.Equal(2, removes.Count);
        Assert.Equal(new[] { 1, 2 }, removes.Select(r => r.OldLineNo!.Value).ToArray());
        Assert.Equal(new[] { "p", "q" }, removes.Select(r => r.Text).ToArray());
        // Remove lines have no new line number.
        Assert.All(removes, r => Assert.Null(r.NewLineNo));
    }

    [Fact]
    public void SingleMiddleLineChange_OneRemoveOneAdd_ContextPreserved()
    {
        var result = LineDiffer.Diff("a\nB\nc", "a\nX\nc");

        var removes = result.Lines.Where(l => l.Op == LineDiffOp.Remove).ToList();
        var adds = result.Lines.Where(l => l.Op == LineDiffOp.Add).ToList();
        var contexts = result.Lines.Where(l => l.Op == LineDiffOp.Context).ToList();

        Assert.Equal(1, result.RemovedCount);
        Assert.Equal(1, result.AddedCount);
        Assert.False(result.Truncated);

        var remove = Assert.Single(removes);
        Assert.Equal("B", remove.Text);
        Assert.Equal(2, remove.OldLineNo);
        Assert.Null(remove.NewLineNo);

        var add = Assert.Single(adds);
        Assert.Equal("X", add.Text);
        Assert.Equal(2, add.NewLineNo);
        Assert.Null(add.OldLineNo);

        // Context lines "a" and "c" carry both line numbers.
        Assert.Equal(2, contexts.Count);
        var ctxA = contexts.Single(c => c.Text == "a");
        Assert.Equal(1, ctxA.OldLineNo);
        Assert.Equal(1, ctxA.NewLineNo);
        var ctxC = contexts.Single(c => c.Text == "c");
        Assert.Equal(3, ctxC.OldLineNo);
        Assert.Equal(3, ctxC.NewLineNo);
    }

    [Fact]
    public void CrlfNormalization_NoChanges()
    {
        var result = LineDiffer.Diff("a\r\nb", "a\nb");

        Assert.Equal(0, result.AddedCount);
        Assert.Equal(0, result.RemovedCount);
        Assert.False(result.Truncated);
    }

    [Fact]
    public void LoneCarriageReturnNormalization_NoChanges()
    {
        var result = LineDiffer.Diff("a\rb", "a\nb");

        Assert.Equal(0, result.AddedCount);
        Assert.Equal(0, result.RemovedCount);
        Assert.False(result.Truncated);
    }

    [Fact]
    public void NullOld_NewSingleLine_OneAdd()
    {
        var result = LineDiffer.Diff(null, "x");

        Assert.Equal(1, result.AddedCount);
        Assert.Equal(0, result.RemovedCount);
        var add = Assert.Single(result.Lines, l => l.Op == LineDiffOp.Add);
        Assert.Equal("x", add.Text);
        Assert.Equal(1, add.NewLineNo);
    }

    [Fact]
    public void NullNew_OldSingleLine_OneRemove()
    {
        var result = LineDiffer.Diff("x", null);

        Assert.Equal(0, result.AddedCount);
        Assert.Equal(1, result.RemovedCount);
        var remove = Assert.Single(result.Lines, l => l.Op == LineDiffOp.Remove);
        Assert.Equal("x", remove.Text);
        Assert.Equal(1, remove.OldLineNo);
    }

    [Fact]
    public void BothNull_NoChanges()
    {
        var result = LineDiffer.Diff(null, null);

        Assert.Equal(0, result.AddedCount);
        Assert.Equal(0, result.RemovedCount);
        Assert.False(result.Truncated);
    }

    [Fact]
    public void SizeCap_OldEmpty_ThousandNewLines_TruncatedToMax()
    {
        var newText = string.Join("\n", Enumerable.Range(1, 1000).Select(i => $"line{i}"));

        var result = LineDiffer.Diff("", newText, maxLines: 400);

        Assert.True(result.Lines.Count <= 400);
        Assert.True(result.Truncated);
        Assert.Equal(1000, result.AddedCount);
        Assert.Equal(0, result.RemovedCount);
    }

    [Fact]
    public void InterleavedCase_LcsContextPreserved()
    {
        // old: 1 2 3 4   new: 1 3 4 5  =>  remove 2, context 1/3/4, add 5
        var result = LineDiffer.Diff("1\n2\n3\n4", "1\n3\n4\n5");

        Assert.Equal(1, result.AddedCount);
        Assert.Equal(1, result.RemovedCount);
        Assert.False(result.Truncated);

        var remove = Assert.Single(result.Lines, l => l.Op == LineDiffOp.Remove);
        Assert.Equal("2", remove.Text);
        Assert.Equal(2, remove.OldLineNo);

        var add = Assert.Single(result.Lines, l => l.Op == LineDiffOp.Add);
        Assert.Equal("5", add.Text);
        Assert.Equal(4, add.NewLineNo);

        var contextTexts = result.Lines
            .Where(l => l.Op == LineDiffOp.Context)
            .Select(l => l.Text)
            .ToList();
        Assert.Equal(new[] { "1", "3", "4" }, contextTexts);

        // Verify ordering: the remove of "2" appears after context "1" and before context "3".
        var ops = result.Lines.Select(l => (l.Op, l.Text)).ToList();
        var idxCtx1 = ops.FindIndex(x => x is { Op: LineDiffOp.Context, Text: "1" });
        var idxRem2 = ops.FindIndex(x => x is { Op: LineDiffOp.Remove, Text: "2" });
        var idxCtx3 = ops.FindIndex(x => x is { Op: LineDiffOp.Context, Text: "3" });
        Assert.True(idxCtx1 < idxRem2 && idxRem2 < idxCtx3);
    }

    [Fact]
    public void TrailingNewline_ProducesTrailingEmptyLine()
    {
        // "a\n" splits to ["a", ""] — both texts identical => no changes.
        var result = LineDiffer.Diff("a\n", "a\n");

        Assert.Equal(0, result.AddedCount);
        Assert.Equal(0, result.RemovedCount);
    }

    [Fact]
    public void OversizedInputs_ZeroCommonLines_DegradesToSummary_WithoutRunningMyers()
    {
        // Two 10,000-line inputs with zero common lines. Running the Myers trace here would
        // record ~20,000 V-snapshots of ~160 KB each (multi-GB transient allocation), so the
        // input-size guard must short-circuit to a summary-only result long before that.
        var oldText = string.Join("\n", Enumerable.Range(1, 10_000).Select(i => $"old-line-{i}"));
        var newText = string.Join("\n", Enumerable.Range(1, 10_000).Select(i => $"new-line-{i}"));

        var sw = Stopwatch.StartNew();
        var result = LineDiffer.Diff(oldText, newText);
        sw.Stop();

        // Summary-only shape: same as the output cap — Truncated, full totals, no hunks.
        Assert.True(result.Truncated);
        Assert.Empty(result.Lines);
        Assert.Equal(10_000, result.AddedCount);
        Assert.Equal(10_000, result.RemovedCount);

        // Must return within a small time budget; pathological slowness is a failure.
        Assert.True(
            sw.Elapsed < TimeSpan.FromSeconds(2),
            $"Diff of oversized inputs took {sw.Elapsed.TotalSeconds:F1}s — the input-size guard did not short-circuit.");
    }
}
