using System.Text.Json;
using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.Usage;

namespace ClaudeDo.Worker.Tests.Usage;

/// <summary>
/// Covers what is left of the reader after the model/scope aggregation moved to TokenTracker:
/// the per-session totals that back <c>task_runs</c>' per-run token delta.
/// </summary>
public class TranscriptUsageReaderTests : IDisposable
{
    private readonly string _root;
    private readonly WorkerConfig _cfg;

    public TranscriptUsageReaderTests()
    {
        _root = Path.Combine(Path.GetTempPath(), $"tur_{Guid.NewGuid():N}");
        Directory.CreateDirectory(_root);
        _cfg = new WorkerConfig
        {
            CentralWorktreeRoot = Path.Combine(_root, "central-worktrees"),
            SandboxRoot = Path.Combine(_root, "sandbox"),
        };
    }

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

    private string WriteSession(string projectDir, string file, params string[] lines)
    {
        var dir = Path.Combine(_root, "projects", projectDir);
        Directory.CreateDirectory(dir);
        var path = Path.Combine(dir, file);
        File.WriteAllLines(path, lines);
        return path;
    }

    private static string AssistantLine(
        string cwd, string ts, string model, long input, long output, long cacheRead, long cacheCreation,
        string? requestId = null, string? messageId = null) =>
        JsonSerializer.Serialize(new Dictionary<string, object?>
        {
            ["type"] = "assistant",
            ["cwd"] = cwd,
            ["timestamp"] = ts,
            ["requestId"] = requestId,
            ["message"] = new Dictionary<string, object?>
            {
                ["id"] = messageId,
                ["model"] = model,
                ["usage"] = new Dictionary<string, object?>
                {
                    ["input_tokens"] = input,
                    ["output_tokens"] = output,
                    ["cache_read_input_tokens"] = cacheRead,
                    ["cache_creation_input_tokens"] = cacheCreation,
                },
            },
        });

    private TranscriptUsageReader MakeReader() =>
        new(_cfg, Path.Combine(_root, "projects"));

    [Fact]
    public async Task ReadSessionTotalsAsync_Sums_All_Assistant_Messages_In_The_Session_File()
    {
        WriteSession("proj", "sess-1.jsonl",
            AssistantLine(@"C:\Dev\App", "2026-06-01T08:00:00Z", "claude-sonnet-5", 10, 20, 100, 5, requestId: "r1"),
            AssistantLine(@"C:\Dev\App", "2026-06-01T09:00:00Z", "claude-sonnet-5", 3, 7, 200, 0, requestId: "r2"));

        var reader = MakeReader();
        var totals = await reader.ReadSessionTotalsAsync("sess-1");

        Assert.NotNull(totals);
        Assert.Equal(13, totals!.InputTokens);
        Assert.Equal(27, totals.OutputTokens);
        Assert.Equal(300, totals.CacheReadTokens);
        Assert.Equal(5, totals.CacheCreationTokens);
    }

    [Fact]
    public async Task ReadSessionTotalsAsync_Skips_Synthetic_Model_Messages()
    {
        WriteSession("proj", "sess-2.jsonl",
            AssistantLine(@"C:\Dev\App", "2026-06-01T08:00:00Z", "claude-sonnet-5", 10, 20, 0, 0, requestId: "r1"),
            AssistantLine(@"C:\Dev\App", "2026-06-01T09:00:00Z", "<synthetic>", 999, 999, 999, 999, requestId: "r2"));

        var reader = MakeReader();
        var totals = await reader.ReadSessionTotalsAsync("sess-2");

        Assert.NotNull(totals);
        Assert.Equal(10, totals!.InputTokens);
        Assert.Equal(20, totals.OutputTokens);
    }

    [Fact]
    public async Task ReadSessionTotalsAsync_Dedupes_By_RequestId_Within_The_Session()
    {
        WriteSession("proj", "sess-3.jsonl",
            AssistantLine(@"C:\Dev\App", "2026-06-01T08:00:00Z", "claude-sonnet-5", 10, 20, 0, 0, requestId: "dup"),
            AssistantLine(@"C:\Dev\App", "2026-06-01T08:00:01Z", "claude-sonnet-5", 10, 20, 0, 0, requestId: "dup"));

        var reader = MakeReader();
        var totals = await reader.ReadSessionTotalsAsync("sess-3");

        Assert.NotNull(totals);
        Assert.Equal(10, totals!.InputTokens);
        Assert.Equal(20, totals.OutputTokens);
    }

    [Fact]
    public async Task ReadSessionTotalsAsync_Malformed_Line_Does_Not_Abort_The_Read()
    {
        WriteSession("proj", "sess-4.jsonl",
            "this is not json",
            AssistantLine(@"C:\Dev\App", "2026-06-01T08:00:00Z", "claude-sonnet-5", 5, 5, 0, 0, requestId: "r1"));

        var reader = MakeReader();
        var totals = await reader.ReadSessionTotalsAsync("sess-4");

        Assert.NotNull(totals);
        Assert.Equal(5, totals!.InputTokens);
    }

    [Fact]
    public async Task ReadSessionTotalsAsync_Returns_Null_When_No_Matching_Transcript_File()
    {
        var reader = MakeReader();
        var totals = await reader.ReadSessionTotalsAsync("does-not-exist");

        Assert.Null(totals);
    }

    [Fact]
    public async Task ReadSessionTotalsAsync_Returns_Null_When_ProjectsRoot_Missing()
    {
        var reader = new TranscriptUsageReader(_cfg, Path.Combine(_root, "does-not-exist"));
        var totals = await reader.ReadSessionTotalsAsync("sess-1");

        Assert.Null(totals);
    }
}
