using System.ComponentModel;
using System.IO;
using ClaudeDo.Localization;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Modals;

namespace ClaudeDo.Ui.Tests.ViewModels;

public class DiffViewerViewModelTests
{
    public DiffViewerViewModelTests()
    {
        var dir = AppContext.BaseDirectory;
        while (dir is not null && !Directory.Exists(Path.Combine(dir, "src", "ClaudeDo.Localization", "locales")))
            dir = Path.GetDirectoryName(dir);
        Loc.Current = new Localizer(
            LocaleStore.Load(Path.Combine(dir!, "src", "ClaudeDo.Localization", "locales")), "en");
    }

    // A throwaway config path — the view toggles call Save(), and a test must never
    // overwrite the developer's real ~/.todo-app/ui.config.json.
    private static AppSettings TestSettings() => new()
    {
        ConfigPath = Path.Combine(Path.GetTempPath(), $"claudedo-uicfg-{Guid.NewGuid():N}.json"),
    };

    private sealed class FakePlanningWorker : StubWorkerClient
    {
        public IReadOnlyList<SubtaskDiffDto> AggregateResult { get; set; } = Array.Empty<SubtaskDiffDto>();
        public CombinedDiffResultDto? CombinedResult { get; set; }
        public string? CombinedException { get; set; }

        public override Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId) =>
            Task.FromResult(AggregateResult);
        public override Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch) =>
            CombinedException is not null
                ? throw new Exception(CombinedException)
                : Task.FromResult(CombinedResult);
    }

    // ── Files mode: commit-range guards (ported from DiffModal) ──

    [Fact]
    public async Task CommitRange_NullHeadCommit_ShowsUnavailable()
    {
        var vm = new DiffViewerViewModel(null!, new FakePlanningWorker(), TestSettings());
        vm.ConfigureCommitRange("/some/repo", "abc123", null);

        await vm.LoadAsync();

        Assert.Empty(vm.FileTree);
        Assert.NotNull(vm.StatusMessage);
        Assert.Contains("no longer available", vm.StatusMessage, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public async Task CommitRange_NullBaseRef_ShowsUnavailable()
    {
        var vm = new DiffViewerViewModel(null!, new FakePlanningWorker(), TestSettings());
        vm.ConfigureCommitRange("/some/repo", null, "def456");

        await vm.LoadAsync();

        Assert.Empty(vm.FileTree);
        Assert.NotNull(vm.StatusMessage);
        Assert.Contains("no longer available", vm.StatusMessage, StringComparison.OrdinalIgnoreCase);
    }

    // ── File tree building ──

    [Fact]
    public void DiffTree_Build_GroupsByFolder_AndCarriesFileLeaves()
    {
        var files = new List<DiffFileViewModel>
        {
            new() { Path = "src/a/x.cs", Additions = 3, Deletions = 1 },
            new() { Path = "src/a/y.cs" },
            new() { Path = "readme.md", Status = DiffFileStatus.Added },
        };

        var roots = DiffTree.Build(files);

        Assert.Equal(2, roots.Count); // "src" dir + "readme.md" leaf
        var src = roots.First(n => n is { IsDirectory: true, Name: "src" });
        var a = Assert.Single(src.Children);
        Assert.True(a is { IsDirectory: true, Name: "a" });
        Assert.Equal(2, a.Children.Count);

        var x = a.Children.First(n => n.Name == "x.cs");
        Assert.False(x.IsDirectory);
        Assert.NotNull(x.File);
        Assert.Equal(3, x.Additions);

        var readme = roots.First(n => n is { IsDirectory: false, Name: "readme.md" });
        Assert.Equal("A", readme.StatusCode);

        // First leaf walks depth-first in insertion order → src/a/x.cs.
        Assert.Equal(x, DiffTree.FirstLeaf(roots));
    }

    // ── Planning mode (ported from PlanningDiff) ──

    [Fact]
    public async Task Planning_Load_PopulatesSubtasks_SelectsFirst()
    {
        var fake = new FakePlanningWorker
        {
            AggregateResult = new[]
            {
                new SubtaskDiffDto("s1", "First",  "branch-1", "base1", "head1", "+1 -0", "diff1"),
                new SubtaskDiffDto("s2", "Second", "branch-2", "base2", "head2", "+2 -1", "diff2"),
            }
        };
        var vm = new DiffViewerViewModel(null!, fake, TestSettings());
        vm.ConfigurePlanning("plan-1", "main");

        await vm.LoadAsync();

        Assert.Equal(2, vm.Subtasks.Count);
        Assert.Equal(vm.Subtasks[0], vm.SelectedSubtask);
    }

    [Fact]
    public async Task Planning_SelectSubtask_SetsDisplayedDiff()
    {
        var fake = new FakePlanningWorker
        {
            AggregateResult = new[]
            {
                new SubtaskDiffDto("s1", "First",  "b1", "base1", "head1", null, "DIFF-A"),
                new SubtaskDiffDto("s2", "Second", "b2", "base2", "head2", null, "DIFF-B"),
            }
        };
        var vm = new DiffViewerViewModel(null!, fake, TestSettings());
        vm.ConfigurePlanning("plan-1", "main");
        await vm.LoadAsync();

        vm.SelectedSubtask = vm.Subtasks[1];

        Assert.Equal("DIFF-B", vm.DisplayedDiff);
    }

    [Fact]
    public async Task Planning_SelectSubtask_PopulatesPlanningFilesAsync()
    {
        const string diff = "diff --git a/foo.cs b/foo.cs\n" +
                             "--- a/foo.cs\n" +
                             "+++ b/foo.cs\n" +
                             "@@ -1,1 +1,1 @@\n" +
                             "-old\n" +
                             "+new\n";
        var fake = new FakePlanningWorker
        {
            AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, diff) }
        };
        var vm = new DiffViewerViewModel(null!, fake, TestSettings());
        vm.ConfigurePlanning("plan-1", "main");

        await vm.LoadAsync();

        // OnDisplayedDiffChanged offloads the parse via Task.Run and writes PlanningFiles back
        // fire-and-forget, so it may not have landed yet when LoadAsync returns.
        var deadline = DateTime.UtcNow.AddSeconds(5);
        while (DateTime.UtcNow < deadline && vm.PlanningFiles.Count == 0) await Task.Delay(10);

        var file = Assert.Single(vm.PlanningFiles);
        Assert.Equal("foo.cs", file.Path);
    }

    private static string DiffFor(string path) =>
        $"diff --git a/{path} b/{path}\n--- a/{path}\n+++ b/{path}\n@@ -1,1 +1,1 @@\n-old\n+new\n";

    [Fact]
    public async Task Planning_SelectSubtask_RunsAndEndsParseOp()
    {
        var fake = new FakePlanningWorker
        {
            AggregateResult = new[]
            {
                new SubtaskDiffDto("s1", "First",  "b1", "base1", "head1", null, DiffFor("a.cs")),
                new SubtaskDiffDto("s2", "Second", "b2", "base2", "head2", null, DiffFor("b.cs")),
            }
        };
        var vm = new DiffViewerViewModel(null!, fake, TestSettings());
        vm.ConfigurePlanning("plan-1", "main");
        await vm.LoadAsync();

        // Let the auto-selected first subtask's parse settle before starting the assertion.
        var settleDeadline = DateTime.UtcNow.AddSeconds(5);
        while (DateTime.UtcNow < settleDeadline && vm.ParseOp.IsRunning) await Task.Delay(10);

        // ParseOp.Begin runs synchronously (before the offloaded parse's first await point can
        // return), so this handler is guaranteed to observe IsRunning=true regardless of how
        // fast the background parse itself completes — no race with the poll loop below.
        var sawRunning = false;
        vm.ParseOp.PropertyChanged += (_, e) =>
        {
            if (e.PropertyName == nameof(OperationStatus.IsRunning) && vm.ParseOp.IsRunning)
                sawRunning = true;
        };

        vm.SelectedSubtask = vm.Subtasks[1];

        var deadline = DateTime.UtcNow.AddSeconds(5);
        while (DateTime.UtcNow < deadline && vm.ParseOp.IsRunning) await Task.Delay(10);

        Assert.True(sawRunning);
        Assert.False(vm.ParseOp.IsRunning);
        var file = Assert.Single(vm.PlanningFiles);
        Assert.Equal("b.cs", file.Path);
    }

    [Fact]
    public async Task Planning_RapidSubtaskSwitches_ParseOpEndsAndOnlyLatestWins()
    {
        var fake = new FakePlanningWorker
        {
            AggregateResult = new[]
            {
                new SubtaskDiffDto("s1", "First",  "b1", "base1", "head1", null, DiffFor("a.cs")),
                new SubtaskDiffDto("s2", "Second", "b2", "base2", "head2", null, DiffFor("b.cs")),
            }
        };
        var vm = new DiffViewerViewModel(null!, fake, TestSettings());
        vm.ConfigurePlanning("plan-1", "main");
        await vm.LoadAsync();

        var settleDeadline = DateTime.UtcNow.AddSeconds(5);
        while (DateTime.UtcNow < settleDeadline && vm.ParseOp.IsRunning) await Task.Delay(10);

        var sawRunning = false;
        vm.ParseOp.PropertyChanged += (_, e) =>
        {
            if (e.PropertyName == nameof(OperationStatus.IsRunning) && vm.ParseOp.IsRunning)
                sawRunning = true;
        };

        // Two changes fired back-to-back without awaiting in between: the first parse is
        // overtaken by the second before it can write PlanningFiles.
        vm.SelectedSubtask = vm.Subtasks[1];
        vm.SelectedSubtask = vm.Subtasks[0];

        var deadline = DateTime.UtcNow.AddSeconds(5);
        while (DateTime.UtcNow < deadline && vm.ParseOp.IsRunning) await Task.Delay(10);

        Assert.True(sawRunning);
        Assert.False(vm.ParseOp.IsRunning);
        var file = Assert.Single(vm.PlanningFiles);
        Assert.Equal("a.cs", file.Path);
    }

    [Fact]
    public async Task Planning_ToggleCombined_Success_DisplaysUnifiedDiff()
    {
        var fake = new FakePlanningWorker
        {
            AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A") },
            CombinedResult = new CombinedDiffResultDto(true, "integration-branch", "COMBINED-DIFF", null, null),
        };
        var vm = new DiffViewerViewModel(null!, fake, TestSettings());
        vm.ConfigurePlanning("plan-1", "main");
        await vm.LoadAsync();

        vm.IsCombinedMode = true;

        var deadline = DateTime.UtcNow.AddSeconds(5);
        while (DateTime.UtcNow < deadline && vm.IsLoadingCombined) await Task.Delay(10);

        Assert.Equal("COMBINED-DIFF", vm.DisplayedDiff);
        Assert.Null(vm.CombinedWarning);
    }

    [Fact]
    public async Task Planning_ToggleCombined_Conflict_ShowsWarning()
    {
        var fake = new FakePlanningWorker
        {
            AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A") },
            CombinedResult = new CombinedDiffResultDto(false, null, null, "subtask-42", new[] { "a.cs", "b.cs" }),
        };
        var vm = new DiffViewerViewModel(null!, fake, TestSettings());
        vm.ConfigurePlanning("plan-1", "main");
        await vm.LoadAsync();

        vm.IsCombinedMode = true;

        var deadline = DateTime.UtcNow.AddSeconds(5);
        while (DateTime.UtcNow < deadline && vm.IsLoadingCombined) await Task.Delay(10);

        Assert.NotNull(vm.CombinedWarning);
        Assert.Contains("subtask-42", vm.CombinedWarning);
        Assert.Contains("2 files", vm.CombinedWarning);
    }

    [Fact]
    public async Task Planning_ToggleCombined_HubReturnsNull_ShowsError()
    {
        var fake = new FakePlanningWorker
        {
            AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A") },
            CombinedResult = null,
        };
        var vm = new DiffViewerViewModel(null!, fake, TestSettings());
        vm.ConfigurePlanning("plan-1", "main");
        await vm.LoadAsync();

        vm.IsCombinedMode = true;

        var deadline = DateTime.UtcNow.AddSeconds(5);
        while (DateTime.UtcNow < deadline && vm.IsLoadingCombined) await Task.Delay(10);

        Assert.NotNull(vm.CombinedWarning);
        Assert.NotEmpty(vm.CombinedWarning!);
    }

    [Fact]
    public async Task Planning_ToggleCombined_WhenWorkerThrows_ShowsExceptionMessage()
    {
        var fake = new FakePlanningWorker
        {
            AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A") },
            CombinedException = "planning task not found",
        };
        var vm = new DiffViewerViewModel(null!, fake, TestSettings());
        vm.ConfigurePlanning("plan-1", "main");
        await vm.LoadAsync();

        vm.IsCombinedMode = true;

        var deadline = DateTime.UtcNow.AddSeconds(5);
        while (DateTime.UtcNow < deadline && vm.IsLoadingCombined) await Task.Delay(10);

        Assert.NotNull(vm.CombinedWarning);
        Assert.Contains("planning task not found", vm.CombinedWarning);
        Assert.Equal("", vm.DisplayedDiff);
    }
}
