using ClaudeDo.Data;
using ClaudeDo.Data.Git;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Runner;
using ClaudeDo.Worker.Tests.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
using Xunit;

namespace ClaudeDo.Worker.Tests.Runner;

public class FailureDiagnosisTests
{
    [Fact]
    public void Max_Turns_Message_Names_The_Limit_And_Requeue()
    {
        var result = new RunResult { ExitCode = 1, TerminalReason = "max_turns", TurnCount = 55 };
        var message = TaskRunner.BuildFailureMarkdown(result, configuredMaxTurns: 60);
        Assert.Contains("55", message);
        Assert.Contains("60", message);
        Assert.Contains("maxTurns", message);
        Assert.Contains("Queue", message);
        Assert.DoesNotContain(ClaudeProcess.NoResultPrefix, message);
    }

    [Fact]
    public void Max_Turns_Message_Without_Configured_Limit_Still_Shows_Actual_Turns()
    {
        var result = new RunResult { ExitCode = 1, TerminalReason = "max_turns", TurnCount = 30 };
        var message = TaskRunner.BuildFailureMarkdown(result, configuredMaxTurns: null);
        Assert.Contains("30", message);
    }

    [Fact]
    public void Api_Error_Passes_Through_Provider_Text_With_Reset_Time()
    {
        var result = new RunResult
        {
            ExitCode = 1,
            TerminalReason = "api_error",
            ResultMarkdown = "You've hit your session limit – resets 1pm (Europe/Berlin)",
            ErrorMarkdown = $"{ClaudeProcess.NoResultPrefix} 1 and no result.",
        };
        var message = TaskRunner.BuildFailureMarkdown(result, configuredMaxTurns: 60);
        Assert.Equal("You've hit your session limit – resets 1pm (Europe/Berlin)", message);
    }

    [Fact]
    public void Api_Error_Falls_Back_To_Errors_List_When_No_Result_Text()
    {
        var result = new RunResult
        {
            ExitCode = 1,
            TerminalReason = "api_error",
            Errors = new[] { "rate limited" },
            ErrorMarkdown = $"{ClaudeProcess.NoResultPrefix} 1 and no result.",
        };
        var message = TaskRunner.BuildFailureMarkdown(result, configuredMaxTurns: 60);
        Assert.Equal("rate limited", message);
    }

    [Fact]
    public void Unknown_Reason_Keeps_Existing_Text_And_Appends_Terminal_Reason()
    {
        var result = new RunResult
        {
            ExitCode = 1,
            TerminalReason = "some_new_reason",
            ErrorMarkdown = "stderr output here",
        };
        var message = TaskRunner.BuildFailureMarkdown(result, configuredMaxTurns: 60);
        Assert.Contains("stderr output here", message);
        Assert.Contains("some_new_reason", message);
    }

    [Fact]
    public void No_Terminal_Reason_Behaves_As_Before()
    {
        var result = new RunResult
        {
            ExitCode = 1,
            ErrorMarkdown = $"{ClaudeProcess.NoResultPrefix} 1 and no result.",
        };
        var message = TaskRunner.BuildFailureMarkdown(result, configuredMaxTurns: 60);
        Assert.Equal($"{ClaudeProcess.NoResultPrefix} 1 and no result.", message);
    }

    [Theory]
    [InlineData("max_turns", "max_turns")]
    [InlineData("timeout", "timeout")]
    [InlineData("api_error", "error")]
    [InlineData("some_new_reason", "error")]
    [InlineData(null, "error")]
    public void ClassifyFailureReason_Maps_TerminalReason_To_McpEnum(string? terminalReason, string expected)
    {
        Assert.Equal(expected, TaskRunner.ClassifyFailureReason(terminalReason));
    }

    [Theory]
    [InlineData("You've hit your session limit – resets 1pm (Europe/Berlin)")]
    [InlineData("Claude usage limit reached, resets at 3pm")]
    [InlineData("rate limited, please retry later")]
    public void ClassifyFailureReason_ApiError_With_Limit_Text_Returns_UsageLimit(string messageText)
    {
        Assert.Equal("usage_limit", TaskRunner.ClassifyFailureReason("api_error", messageText));
    }

    [Fact]
    public void ClassifyFailureReason_ApiError_With_Generic_Text_Stays_Error()
    {
        Assert.Equal("error", TaskRunner.ClassifyFailureReason("api_error", "Internal server error, please retry"));
    }

    [Theory]
    [InlineData("max_turns")]
    [InlineData("timeout")]
    public void ClassifyFailureReason_Ignores_Limit_Text_For_NonApiError_Reasons(string terminalReason)
    {
        Assert.Equal(terminalReason, TaskRunner.ClassifyFailureReason(terminalReason, "usage limit reached"));
    }
}

public sealed class FailureDiagnosisEndToEndTests : IDisposable
{
    private readonly DbFixture _db = new();
    private readonly WorkerConfig _cfg;
    private readonly string _tempDir;

    public FailureDiagnosisEndToEndTests()
    {
        _tempDir = Path.Combine(Path.GetTempPath(), $"cd_faildiag_{Guid.NewGuid():N}");
        Directory.CreateDirectory(_tempDir);
        _cfg = new WorkerConfig { SandboxRoot = _tempDir, LogRoot = _tempDir };
    }

    public void Dispose() { _db.Dispose(); try { Directory.Delete(_tempDir, true); } catch { } }

    private TaskRunner MakeRunner(IDbContextFactory<ClaudeDoDbContext> dbFactory, FakeClaudeProcess fake)
    {
        var state = TaskStateServiceBuilder.Build(dbFactory).State;
        var wt = new WorktreeManager(new GitService(), dbFactory, _cfg, NullLogger<WorktreeManager>.Instance);
        return new TaskRunner(fake, dbFactory, new HubBroadcaster(new CapturingHubContext()), wt,
            new ClaudeArgsBuilder(), _cfg, NullLogger<TaskRunner>.Instance, state, new TaskRunTokenRegistry(),
            new AttachmentStore(), new FakeSessionSkillSeeder(), new FakeTranscriptUsageReader());
    }

    [Fact]
    public async Task Max_Turns_Failure_Surfaces_Specific_Message_On_Task_And_Run()
    {
        var dbFactory = _db.CreateFactory();
        using (var ctx = _db.CreateContext())
        {
            ctx.Lists.Add(new ListEntity { Id = "l1", Name = "L", WorkingDir = null, CreatedAt = DateTime.UtcNow });
            ctx.Tasks.Add(new TaskEntity { Id = "t1", ListId = "l1", Title = "T", MaxTurns = 60,
                Status = TaskStatus.Running, CreatedAt = DateTime.UtcNow });
            await ctx.SaveChangesAsync();
        }
        var fake = new FakeClaudeProcess((_, _, _, _, _) => Task.FromResult(new RunResult
        {
            ExitCode = 1,
            TerminalReason = "max_turns",
            Errors = new[] { "Reached maximum number of turns (60)" },
            TurnCount = 60,
        }));
        var runner = MakeRunner(dbFactory, fake);

        using (var ctx = _db.CreateContext())
            await runner.RunAsync((await new TaskRepository(ctx).GetByIdAsync("t1"))!, "slot-1", default, alreadyClaimed: true);

        using var verify = _db.CreateContext();
        var task = await new TaskRepository(verify).GetByIdAsync("t1");
        Assert.Equal(TaskStatus.Failed, task!.Status);
        Assert.Contains("Turn-Budget", task.Result);
        Assert.Contains("60", task.Result);
        Assert.DoesNotContain(ClaudeProcess.NoResultPrefix, task.Result);

        var run = await new TaskRunRepository(verify).GetLatestByTaskIdAsync("t1");
        Assert.Contains("Turn-Budget", run!.ErrorMarkdown);
    }

    [Fact]
    public async Task Api_Error_Failure_Surfaces_Provider_Text_With_Reset_Time()
    {
        var dbFactory = _db.CreateFactory();
        using (var ctx = _db.CreateContext())
        {
            ctx.Lists.Add(new ListEntity { Id = "l1", Name = "L", WorkingDir = null, CreatedAt = DateTime.UtcNow });
            ctx.Tasks.Add(new TaskEntity { Id = "t1", ListId = "l1", Title = "T",
                Status = TaskStatus.Running, CreatedAt = DateTime.UtcNow });
            await ctx.SaveChangesAsync();
        }
        var fake = new FakeClaudeProcess((_, _, _, _, _) => Task.FromResult(new RunResult
        {
            ExitCode = 1,
            TerminalReason = "api_error",
            ResultMarkdown = "You've hit your session limit – resets 1pm (Europe/Berlin)",
        }));
        var runner = MakeRunner(dbFactory, fake);

        using (var ctx = _db.CreateContext())
            await runner.RunAsync((await new TaskRepository(ctx).GetByIdAsync("t1"))!, "slot-1", default, alreadyClaimed: true);

        using var verify = _db.CreateContext();
        var task = await new TaskRepository(verify).GetByIdAsync("t1");
        Assert.Equal(TaskStatus.Failed, task!.Status);
        Assert.Contains("session limit", task.Result);
        Assert.Contains("Europe/Berlin", task.Result);
    }

    [Fact]
    public async Task Crash_Without_Result_Line_Keeps_Generic_Message()
    {
        var dbFactory = _db.CreateFactory();
        using (var ctx = _db.CreateContext())
        {
            ctx.Lists.Add(new ListEntity { Id = "l1", Name = "L", WorkingDir = null, CreatedAt = DateTime.UtcNow });
            ctx.Tasks.Add(new TaskEntity { Id = "t1", ListId = "l1", Title = "T",
                Status = TaskStatus.Running, CreatedAt = DateTime.UtcNow });
            await ctx.SaveChangesAsync();
        }
        var fake = new FakeClaudeProcess((_, _, _, _, _) => Task.FromResult(new RunResult
        {
            ExitCode = 1,
            ErrorMarkdown = $"{ClaudeProcess.NoResultPrefix} 1 and no result.",
        }));
        var runner = MakeRunner(dbFactory, fake);

        using (var ctx = _db.CreateContext())
            await runner.RunAsync((await new TaskRepository(ctx).GetByIdAsync("t1"))!, "slot-1", default, alreadyClaimed: true);

        using var verify = _db.CreateContext();
        var task = await new TaskRepository(verify).GetByIdAsync("t1");
        Assert.Equal(TaskStatus.Failed, task!.Status);
        Assert.Equal($"{ClaudeProcess.NoResultPrefix} 1 and no result.", task.Result);
    }

    // The bug this feature fixes: get_task/batch_get_tasks must be able to tell error_max_turns
    // apart from a real failure without pulling get_task_log. These two tests land a run of each
    // kind and check the classified failureReason (plus turns/budget) that ends up on the task —
    // exactly what those MCP tools read.
    [Fact]
    public async Task Max_Turns_Run_Sets_FailureReason_MaxTurns_With_Turns_And_Budget()
    {
        var dbFactory = _db.CreateFactory();
        using (var ctx = _db.CreateContext())
        {
            ctx.Lists.Add(new ListEntity { Id = "l1", Name = "L", WorkingDir = null, CreatedAt = DateTime.UtcNow });
            ctx.Tasks.Add(new TaskEntity { Id = "t1", ListId = "l1", Title = "T", MaxTurns = 60,
                Status = TaskStatus.Running, CreatedAt = DateTime.UtcNow });
            await ctx.SaveChangesAsync();
        }
        var fake = new FakeClaudeProcess((_, _, _, _, _) => Task.FromResult(new RunResult
        {
            ExitCode = 1,
            TerminalReason = "max_turns",
            ResultSubtype = "error_max_turns",
            Errors = new[] { "Reached maximum number of turns (60)" },
            TurnCount = 60,
        }));
        var runner = MakeRunner(dbFactory, fake);

        using (var ctx = _db.CreateContext())
            await runner.RunAsync((await new TaskRepository(ctx).GetByIdAsync("t1"))!, "slot-1", default, alreadyClaimed: true);

        using var verify = _db.CreateContext();
        var task = await new TaskRepository(verify).GetByIdAsync("t1");
        Assert.Equal(TaskStatus.Failed, task!.Status);
        Assert.Equal("max_turns", task.FailureReason);
        Assert.Equal(60, task.FailureTurnsUsed);
        Assert.Equal(60, task.FailureMaxTurns);

        var run = await new TaskRunRepository(verify).GetLatestByTaskIdAsync("t1");
        Assert.Equal("max_turns", run!.TerminalReason);
        Assert.Equal("error_max_turns", run.ResultSubtype);
    }

    [Fact]
    public async Task Real_Error_Run_Sets_FailureReason_Error_Distinct_From_MaxTurns()
    {
        var dbFactory = _db.CreateFactory();
        using (var ctx = _db.CreateContext())
        {
            ctx.Lists.Add(new ListEntity { Id = "l1", Name = "L", WorkingDir = null, CreatedAt = DateTime.UtcNow });
            ctx.Tasks.Add(new TaskEntity { Id = "t1", ListId = "l1", Title = "T",
                Status = TaskStatus.Running, CreatedAt = DateTime.UtcNow });
            await ctx.SaveChangesAsync();
        }
        var fake = new FakeClaudeProcess((_, _, _, _, _) => Task.FromResult(new RunResult
        {
            ExitCode = 1,
            ErrorMarkdown = $"{ClaudeProcess.NoResultPrefix} 1 and no result.",
        }));
        var runner = MakeRunner(dbFactory, fake);

        using (var ctx = _db.CreateContext())
            await runner.RunAsync((await new TaskRepository(ctx).GetByIdAsync("t1"))!, "slot-1", default, alreadyClaimed: true);

        using var verify = _db.CreateContext();
        var task = await new TaskRepository(verify).GetByIdAsync("t1");
        Assert.Equal(TaskStatus.Failed, task!.Status);
        Assert.Equal("error", task.FailureReason);
        Assert.NotEqual("max_turns", task.FailureReason);
    }

    [Fact]
    public async Task Usage_Limit_Run_Sets_FailureReason_UsageLimit()
    {
        var dbFactory = _db.CreateFactory();
        using (var ctx = _db.CreateContext())
        {
            ctx.Lists.Add(new ListEntity { Id = "l1", Name = "L", WorkingDir = null, CreatedAt = DateTime.UtcNow });
            ctx.Tasks.Add(new TaskEntity { Id = "t1", ListId = "l1", Title = "T",
                Status = TaskStatus.Running, CreatedAt = DateTime.UtcNow });
            await ctx.SaveChangesAsync();
        }
        var fake = new FakeClaudeProcess((_, _, _, _, _) => Task.FromResult(new RunResult
        {
            ExitCode = 1,
            TerminalReason = "api_error",
            ResultMarkdown = "You've hit your session limit – resets 1pm (Europe/Berlin)",
        }));
        var runner = MakeRunner(dbFactory, fake);

        using (var ctx = _db.CreateContext())
            await runner.RunAsync((await new TaskRepository(ctx).GetByIdAsync("t1"))!, "slot-1", default, alreadyClaimed: true);

        using var verify = _db.CreateContext();
        var task = await new TaskRepository(verify).GetByIdAsync("t1");
        Assert.Equal(TaskStatus.Failed, task!.Status);
        Assert.Equal("usage_limit", task.FailureReason);
    }
}
