using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.External;
using ClaudeDo.Worker.Tests.Infrastructure;

namespace ClaudeDo.Worker.Tests.External;

public sealed class RunHistoryMcpToolsTests : IDisposable
{
    private readonly DbFixture _db = new();
    private readonly ClaudeDoDbContext _ctx;
    private readonly TaskRunRepository _runs;
    private readonly TaskRepository _tasks;
    private readonly RunHistoryMcpTools _sut;

    public RunHistoryMcpToolsTests()
    {
        _ctx = _db.CreateContext();
        _runs = new TaskRunRepository(_ctx);
        _tasks = new TaskRepository(_ctx);
        _sut = new RunHistoryMcpTools(_runs, _tasks);
    }

    public void Dispose() { _ctx.Dispose(); _db.Dispose(); }

    private async Task SeedTaskAsync(string taskId)
    {
        var lists = new ListRepository(_ctx);
        var tasks = new TaskRepository(_ctx);
        var listId = Guid.NewGuid().ToString();
        await lists.AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });
        await tasks.AddAsync(new TaskEntity
        {
            Id = taskId, ListId = listId, Title = "t",
            Status = ClaudeDo.Data.Models.TaskStatus.Done, CreatedAt = DateTime.UtcNow, CommitType = "chore",
        });
    }

    [Fact]
    public async Task ListRuns_ReturnsProjectedRuns()
    {
        var taskId = Guid.NewGuid().ToString();
        await SeedTaskAsync(taskId);
        await _runs.AddAsync(new TaskRunEntity
        {
            Id = Guid.NewGuid().ToString(), TaskId = taskId, RunNumber = 1,
            IsRetry = false, Prompt = "p", ResultMarkdown = "done", TokensIn = 10, TokensOut = 20,
        });

        var list = await _sut.ListRuns(taskId, CancellationToken.None);

        Assert.Single(list);
        Assert.Equal("done", list[0].ResultMarkdown);
        Assert.Equal(10, list[0].TokensIn);
    }

    [Fact]
    public async Task GetTaskLog_NoRun_ReturnsUnavailable()
    {
        var taskId = Guid.NewGuid().ToString();
        await SeedTaskAsync(taskId);

        var result = await _sut.GetTaskLog(taskId, cancellationToken: CancellationToken.None);

        Assert.False(result.Available);
        Assert.Empty(result.Entries);
        Assert.Equal(0, result.TotalLines);
    }

    [Fact]
    public async Task GetTaskLog_ReadsLatestRunLogFile()
    {
        var taskId = Guid.NewGuid().ToString();
        await SeedTaskAsync(taskId);
        var logPath = Path.Combine(Path.GetTempPath(), $"claudedo_log_{Guid.NewGuid():N}.txt");
        await File.WriteAllTextAsync(logPath, "line1\nline2\nline3");
        await _runs.AddAsync(new TaskRunEntity
        {
            Id = Guid.NewGuid().ToString(), TaskId = taskId, RunNumber = 1,
            IsRetry = false, Prompt = "p", LogPath = logPath,
        });

        TaskLogResult result;
        try
        {
            result = await _sut.GetTaskLog(taskId, cancellationToken: CancellationToken.None);
        }
        finally
        {
            File.Delete(logPath);
        }

        Assert.True(result.Available);
        Assert.Equal(3, result.TotalLines);
        Assert.Contains("line1", result.Entries);
    }

    [Fact]
    public async Task GetRun_NotFound_Throws()
    {
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            _sut.GetRun("missing", CancellationToken.None));
    }

    [Fact]
    public async Task GetTaskLog_RunExistsButNoLogPath_ReturnsUnavailable()
    {
        var taskId = Guid.NewGuid().ToString();
        await SeedTaskAsync(taskId);
        await _runs.AddAsync(new TaskRunEntity
        {
            Id = Guid.NewGuid().ToString(), TaskId = taskId, RunNumber = 1,
            IsRetry = false, Prompt = "p", LogPath = null,
        });

        var result = await _sut.GetTaskLog(taskId, cancellationToken: CancellationToken.None);

        Assert.False(result.Available);
        Assert.Empty(result.Entries);
    }

    [Fact]
    public async Task GetTaskLog_ManyLines_DefaultTailReturnsLast50()
    {
        var taskId = Guid.NewGuid().ToString();
        await SeedTaskAsync(taskId);
        var logPath = Path.Combine(Path.GetTempPath(), $"claudedo_log_{Guid.NewGuid():N}.txt");

        // Write 108 lines (the observed real-world size that exceeded token limits)
        var lines = Enumerable.Range(1, 108).Select(i => $"{{\"line\":{i}}}");
        await File.WriteAllLinesAsync(logPath, lines);

        await _runs.AddAsync(new TaskRunEntity
        {
            Id = Guid.NewGuid().ToString(), TaskId = taskId, RunNumber = 1,
            IsRetry = false, Prompt = "p", LogPath = logPath,
        });

        TaskLogResult result;
        try
        {
            result = await _sut.GetTaskLog(taskId, cancellationToken: CancellationToken.None);
        }
        finally
        {
            File.Delete(logPath);
        }

        Assert.True(result.Available);
        Assert.True(result.Truncated);
        Assert.Equal(108, result.TotalLines);
        Assert.Equal(50, result.Entries.Count);
        Assert.Contains("{\"line\":108}", result.Entries);    // last line is present
        Assert.DoesNotContain("{\"line\":1}", result.Entries); // first line is not
    }

    [Fact]
    public async Task GetTaskLog_TailParam_ReturnsRequestedCount()
    {
        var taskId = Guid.NewGuid().ToString();
        await SeedTaskAsync(taskId);
        var logPath = Path.Combine(Path.GetTempPath(), $"claudedo_log_{Guid.NewGuid():N}.txt");
        var lines = Enumerable.Range(1, 20).Select(i => $"line{i}");
        await File.WriteAllLinesAsync(logPath, lines);

        await _runs.AddAsync(new TaskRunEntity
        {
            Id = Guid.NewGuid().ToString(), TaskId = taskId, RunNumber = 1,
            IsRetry = false, Prompt = "p", LogPath = logPath,
        });

        TaskLogResult result;
        try
        {
            result = await _sut.GetTaskLog(taskId, tail: 5, cancellationToken: CancellationToken.None);
        }
        finally
        {
            File.Delete(logPath);
        }

        Assert.True(result.Available);
        Assert.True(result.Truncated);
        Assert.Equal(5, result.Entries.Count);
        Assert.Equal("line20", result.Entries[^1]);
    }

    [Fact]
    public async Task GetTaskLog_OffsetLimit_ReturnsSlice()
    {
        var taskId = Guid.NewGuid().ToString();
        await SeedTaskAsync(taskId);
        var logPath = Path.Combine(Path.GetTempPath(), $"claudedo_log_{Guid.NewGuid():N}.txt");
        var lines = Enumerable.Range(1, 10).Select(i => $"line{i}");
        await File.WriteAllLinesAsync(logPath, lines);

        await _runs.AddAsync(new TaskRunEntity
        {
            Id = Guid.NewGuid().ToString(), TaskId = taskId, RunNumber = 1,
            IsRetry = false, Prompt = "p", LogPath = logPath,
        });

        TaskLogResult result;
        try
        {
            result = await _sut.GetTaskLog(taskId, offset: 2, limit: 3, cancellationToken: CancellationToken.None);
        }
        finally
        {
            File.Delete(logPath);
        }

        Assert.True(result.Available);
        Assert.Equal(3, result.Entries.Count);
        Assert.Equal("line3", result.Entries[0]);
        Assert.Equal("line5", result.Entries[^1]);
        Assert.True(result.Truncated);
    }
}
