using System.Net;
using System.Text.RegularExpressions;
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Worker;
using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.Tests.Infrastructure;
using ClaudeDo.Worker.Tickets;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Worker.Tests.Tickets;

public sealed class TicketStatusSyncTests : IDisposable
{
    private readonly DbFixture _db = new();
    private readonly TestDbContextFactory _factory;
    private readonly string _listId = Guid.NewGuid().ToString();
    private readonly List<string> _patDirs = new();
    private int _numberSeed;

    public TicketStatusSyncTests()
    {
        _factory = _db.CreateFactory();
        using var ctx = _factory.CreateDbContext();
        ctx.Lists.Add(new ListEntity
        {
            Id = _listId,
            Name = "Test",
            CreatedAt = DateTime.UtcNow,
            DefaultCommitType = "chore",
        });
        ctx.SaveChanges();
    }

    public void Dispose()
    {
        _db.Dispose();
        foreach (var dir in _patDirs)
        {
            try { Directory.Delete(dir, recursive: true); } catch { /* best effort */ }
        }
    }

    [Fact]
    public async Task Writes_InBearbeitung_when_the_task_starts_running()
    {
        var (sync, calls) = Harness(configured: true);
        var taskId = await SeedTaskAsync(TaskStatus.Running, ticketRef: "bandel:12");

        await sync.SyncAsync(taskId, CancellationToken.None);

        Assert.Equal((12, 2), Assert.Single(calls));
    }

    [Fact]
    public async Task Writes_Fertig_when_the_task_is_done()
    {
        var (sync, calls) = Harness(configured: true);
        var taskId = await SeedTaskAsync(TaskStatus.Done, ticketRef: "bandel:12");

        await sync.SyncAsync(taskId, CancellationToken.None);

        Assert.Equal((12, 3), Assert.Single(calls));
    }

    [Fact]
    public async Task Does_nothing_without_a_ticket_ref()
    {
        var (sync, calls) = Harness(configured: true);
        var taskId = await SeedTaskAsync(TaskStatus.Running, ticketRef: null);

        await sync.SyncAsync(taskId, CancellationToken.None);

        Assert.Empty(calls);
    }

    [Fact]
    public async Task Does_nothing_when_not_configured()
    {
        var (sync, calls) = Harness(configured: false);
        var taskId = await SeedTaskAsync(TaskStatus.Running, ticketRef: "bandel:12");

        await sync.SyncAsync(taskId, CancellationToken.None);

        Assert.Empty(calls);
    }

    [Fact]
    public async Task Does_nothing_for_an_unmapped_status()
    {
        var (sync, calls) = Harness(configured: true);
        var taskId = await SeedTaskAsync(TaskStatus.Queued, ticketRef: "bandel:12");

        await sync.SyncAsync(taskId, CancellationToken.None);

        Assert.Empty(calls);
    }

    [Fact]
    public async Task Skips_the_redundant_patch_from_Running_to_WaitingForReview()
    {
        var (sync, calls) = Harness(configured: true);
        var taskId = await SeedTaskAsync(TaskStatus.Running, ticketRef: "bandel:12");

        await sync.SyncAsync(taskId, CancellationToken.None);
        await SetStatusAsync(taskId, TaskStatus.WaitingForReview);
        await sync.SyncAsync(taskId, CancellationToken.None);

        Assert.Single(calls);   // beide mappen auf 2 — nur ein PATCH
    }

    [Fact]
    public async Task Api_failure_does_not_propagate()
    {
        var (sync, _) = Harness(configured: true, throwOnPatch: true);
        var taskId = await SeedTaskAsync(TaskStatus.Done, ticketRef: "bandel:12");

        // Darf NICHT werfen: ein Ticketsystem-Ausfall hält die Queue nicht an.
        await sync.SyncAsync(taskId, CancellationToken.None);
    }

    // ── Helfer ──────────────────────────────────────────────────────────────

    private sealed class StubHttpClientFactory : IHttpClientFactory
    {
        private readonly StubHandler _handler;
        public StubHttpClientFactory(StubHandler handler) => _handler = handler;
        public HttpClient CreateClient(string name) => new(_handler);
    }

    private (TicketStatusSync Sync, List<(int TicketId, int StatusId)> Calls) Harness(
        bool configured, bool throwOnPatch = false)
    {
        var calls = new List<(int TicketId, int StatusId)>();
        StubHandler handler = null!;
        handler = new StubHandler(request =>
        {
            if (throwOnPatch)
                return (HttpStatusCode.InternalServerError, """{"success":false,"data":null,"message":"boom"}""");

            var urlMatch = Regex.Match(request.RequestUri!.AbsolutePath, @"/api/Ticket/(\d+)/status");
            var ticketId = int.Parse(urlMatch.Groups[1].Value);
            var bodyMatch = Regex.Match(handler.Bodies[^1], "\"statusId\":(\\d+)");
            var statusId = int.Parse(bodyMatch.Groups[1].Value);
            calls.Add((ticketId, statusId));

            return (HttpStatusCode.OK, """{"success":true,"data":{},"message":"ok"}""");
        });

        var patDir = Path.Combine(Path.GetTempPath(), "cdo-ticketsync-" + Guid.NewGuid().ToString("N"));
        _patDirs.Add(patDir);
        var patStore = new TicketPatStore(new DpapiTokenStore(Path.Combine(patDir, "ticket.pat")));
        var worker = new WorkerConfig();
        if (configured)
        {
            worker.TicketApiBaseUrl = "http://api.local";
            patStore.Store.Save("tsp_x");
        }

        var config = new TicketSystemConfig(worker, patStore);
        var clients = new TicketClientFactory(config, new StubHttpClientFactory(handler), NullLogger<BandelTicketClient>.Instance);
        var sync = new TicketStatusSync(config, clients, _factory, NullLogger<TicketStatusSync>.Instance);
        return (sync, calls);
    }

    private async Task<string> SeedTaskAsync(TaskStatus status, string? ticketRef)
    {
        var id = Guid.NewGuid().ToString();
        await using var ctx = _factory.CreateDbContext();
        ctx.Tasks.Add(new TaskEntity
        {
            Id = id,
            ListId = _listId,
            Title = "task",
            Number = ++_numberSeed,
            Status = status,
            CreatedAt = DateTime.UtcNow,
            TicketRef = ticketRef,
        });
        await ctx.SaveChangesAsync();
        return id;
    }

    private async Task SetStatusAsync(string taskId, TaskStatus status)
    {
        await using var ctx = _factory.CreateDbContext();
        await ctx.Tasks.Where(t => t.Id == taskId).ExecuteUpdateAsync(s => s.SetProperty(t => t.Status, status));
    }
}
