using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Online;
using ClaudeDo.Worker.Tests.Infrastructure;
using Microsoft.Extensions.Logging.Abstractions;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Worker.Tests.Online;

/// <summary>
/// Integration tests for <see cref="OnlineSyncService"/> using a fake API + real SQLite.
/// </summary>
public sealed class OnlineSyncServiceTests : IDisposable
{
    private readonly DbFixture _db = new();

    public void Dispose() => _db.Dispose();

    // ---- fake API ----

    private sealed class FakeApi : IOnlineInboxApi
    {
        public List<RemoteTask> UnimportedTasks { get; set; } = [];
        public List<RemoteList> ReceivedLists { get; } = [];
        public List<MirrorTask> ReceivedMirror { get; } = [];
        public List<string> MarkedImported { get; } = [];
        public int CallCount { get; private set; }

        public Task PutListsAsync(IReadOnlyList<RemoteList> lists, CancellationToken ct = default)
        {
            CallCount++;
            ReceivedLists.AddRange(lists);
            return Task.CompletedTask;
        }

        public Task<IReadOnlyList<RemoteTask>> GetUnimportedTasksAsync(CancellationToken ct = default)
        {
            CallCount++;
            return Task.FromResult<IReadOnlyList<RemoteTask>>(UnimportedTasks);
        }

        public Task MarkImportedAsync(string id, CancellationToken ct = default)
        {
            CallCount++;
            MarkedImported.Add(id);
            return Task.CompletedTask;
        }

        public Task PutMirrorAsync(IReadOnlyList<MirrorTask> tasks, CancellationToken ct = default)
        {
            CallCount++;
            ReceivedMirror.AddRange(tasks);
            return Task.CompletedTask;
        }
    }

    private OnlineSyncService BuildService(
        FakeApi api, string? token = "test-token", bool enabled = true, HubBroadcaster? broadcaster = null)
    {
        var config = new OnlineInboxConfig { Enabled = enabled, PollIntervalSeconds = 60 };
        var auth = new StaticTokenAuthProvider(token);
        return new OnlineSyncService(
            _db.CreateFactory(),
            api,
            auth,
            config,
            NullLogger<OnlineSyncService>.Instance,
            broadcaster ?? new HubBroadcaster(new CapturingHubContext()));
    }

    private async Task<(string ListId, ClaudeDoDbContext Ctx, TaskRepository Tasks, ListRepository Lists)> SeedAsync()
    {
        var ctx = _db.CreateContext();
        var lists = new ListRepository(ctx);
        var tasks = new TaskRepository(ctx);
        var listId = Guid.NewGuid().ToString();
        await lists.AddAsync(new ListEntity { Id = listId, Name = "MyList", CreatedAt = DateTime.UtcNow });
        return (listId, ctx, tasks, lists);
    }

    // ---- pull → import → flag ----

    [Fact]
    public async Task Tick_Imports_RemoteTask_And_MarksImported()
    {
        var (listId, ctx, tasks, _) = await SeedAsync();
        using var _ = ctx;

        var remoteId = Guid.NewGuid().ToString();
        var api = new FakeApi
        {
            UnimportedTasks = [new RemoteTask(remoteId, listId, "From Web", "desc", DateTimeOffset.UtcNow)],
        };
        var svc = BuildService(api);

        await svc.TickAsync(CancellationToken.None);

        var imported = await tasks.GetByIdAsync(remoteId);
        Assert.NotNull(imported);
        Assert.Equal("From Web", imported!.Title);
        Assert.Equal("desc", imported.Description);
        Assert.Equal(TaskStatus.Idle, imported.Status);
        Assert.Equal("online", imported.CreatedBy);
        Assert.Contains(remoteId, api.MarkedImported);
    }

    [Fact]
    public async Task Tick_Imports_RemoteTask_BroadcastsTaskUpdated()
    {
        var (listId, ctx, _, _) = await SeedAsync();
        using var _ = ctx;

        var remoteId = Guid.NewGuid().ToString();
        var api = new FakeApi
        {
            UnimportedTasks = [new RemoteTask(remoteId, listId, "From Web", "desc", DateTimeOffset.UtcNow)],
        };
        var hubContext = new CapturingHubContext();
        var svc = BuildService(api, broadcaster: new HubBroadcaster(hubContext));

        await svc.TickAsync(CancellationToken.None);

        Assert.Contains(hubContext.Proxy.Calls,
            c => c.Method == "TaskUpdated" && (string)c.Args[0]! == remoteId);
    }

    [Fact]
    public async Task Tick_UnknownList_Skips_And_DoesNotMark()
    {
        var _ = await SeedAsync();
        var remoteId = Guid.NewGuid().ToString();
        var api = new FakeApi
        {
            UnimportedTasks = [new RemoteTask(remoteId, "unknown-list-id", "T", null, DateTimeOffset.UtcNow)],
        };
        var svc = BuildService(api);

        await svc.TickAsync(CancellationToken.None);

        using var ctx = _db.CreateContext();
        var tasks = new TaskRepository(ctx);
        Assert.Null(await tasks.GetByIdAsync(remoteId));
        Assert.DoesNotContain(remoteId, api.MarkedImported);
    }

    // ---- mirror == Idle backlog ----

    [Fact]
    public async Task Tick_Mirror_Contains_Idle_Backlog()
    {
        var (listId, ctx, tasks, _) = await SeedAsync();
        using var _ = ctx;

        // Idle task → should appear in mirror
        var idle = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(), ListId = listId, Title = "Idle Task",
            Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow,
        };
        await tasks.AddAsync(idle);

        // Queued task → must NOT appear
        var queued = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(), ListId = listId, Title = "Queued",
            Status = TaskStatus.Queued, CreatedAt = DateTime.UtcNow,
        };
        await tasks.AddAsync(queued);

        var api = new FakeApi();
        var svc = BuildService(api);
        await svc.TickAsync(CancellationToken.None);

        var mirrorIds = api.ReceivedMirror.Select(m => m.Id).ToHashSet();
        Assert.Contains(idle.Id, mirrorIds);
        Assert.DoesNotContain(queued.Id, mirrorIds);
    }

    [Fact]
    public async Task Tick_ImportedTask_IncludedInMirror()
    {
        // Newly imported tasks must be part of the mirror sent in the same cycle (order matters).
        var (listId, ctx, tasks, _) = await SeedAsync();
        using var _ = ctx;

        var remoteId = Guid.NewGuid().ToString();
        var api = new FakeApi
        {
            UnimportedTasks = [new RemoteTask(remoteId, listId, "New", null, DateTimeOffset.UtcNow)],
        };
        var svc = BuildService(api);
        await svc.TickAsync(CancellationToken.None);

        // Imported task lands Idle → must be in the mirror payload
        Assert.Contains(api.ReceivedMirror, m => m.Id == remoteId);
    }

    // ---- lists pushed ----

    [Fact]
    public async Task Tick_Pushes_AllLists()
    {
        var (listId, ctx, _, lists) = await SeedAsync();
        using var _ = ctx;

        // Add a second list
        var listId2 = Guid.NewGuid().ToString();
        await lists.AddAsync(new ListEntity { Id = listId2, Name = "List2", CreatedAt = DateTime.UtcNow });

        var api = new FakeApi();
        var svc = BuildService(api);
        await svc.TickAsync(CancellationToken.None);

        var pushedIds = api.ReceivedLists.Select(l => l.Id).ToHashSet();
        Assert.Contains(listId, pushedIds);
        Assert.Contains(listId2, pushedIds);
    }

    // ---- no token = no calls ----

    [Fact]
    public async Task Tick_NoToken_SkipsCycle_NoApiCalls()
    {
        _ = await SeedAsync();
        var api = new FakeApi();
        var svc = BuildService(api, token: null);

        await svc.TickAsync(CancellationToken.None);

        Assert.Equal(0, api.CallCount);
    }

    [Fact]
    public async Task Tick_Disabled_SkipsCycle_NoApiCalls()
    {
        _ = await SeedAsync();
        var api = new FakeApi();
        var svc = BuildService(api, enabled: false);

        await svc.TickAsync(CancellationToken.None);

        Assert.Equal(0, api.CallCount);
    }

    // ---- multi-user: owner stamping + guard ----

    [Fact]
    public async Task Tick_StampsOwnerId_OnPushedListsAndMirror()
    {
        var (listId, ctx, tasks, _) = await SeedAsync();
        using var _ = ctx;
        await tasks.AddAsync(new TaskEntity
        {
            Id = Guid.NewGuid().ToString(), ListId = listId, Title = "Idle",
            Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow,
        });

        var token = JwtClaimsTests.MakeToken("{\"sub\":\"owner-1\"}");
        var api = new FakeApi();
        var svc = BuildService(api, token);

        await svc.TickAsync(CancellationToken.None);

        Assert.All(api.ReceivedLists, l => Assert.Equal("owner-1", l.OwnerId));
        Assert.NotEmpty(api.ReceivedMirror);
        Assert.All(api.ReceivedMirror, m => Assert.Equal("owner-1", m.OwnerId));
    }

    [Fact]
    public async Task Tick_SkipsRemoteTask_OwnedByAnotherUser()
    {
        var (listId, ctx, tasks, _) = await SeedAsync();
        using var _ = ctx;

        var foreignId = Guid.NewGuid().ToString();
        var api = new FakeApi
        {
            UnimportedTasks =
            [
                new RemoteTask(foreignId, listId, "Theirs", null, DateTimeOffset.UtcNow, OwnerId: "owner-2"),
            ],
        };
        var svc = BuildService(api, JwtClaimsTests.MakeToken("{\"sub\":\"owner-1\"}"));

        await svc.TickAsync(CancellationToken.None);

        Assert.Null(await tasks.GetByIdAsync(foreignId));
        Assert.DoesNotContain(foreignId, api.MarkedImported);
    }

    [Fact]
    public async Task Tick_Imports_OwnTask_And_UnownedTask()
    {
        var (listId, ctx, tasks, _) = await SeedAsync();
        using var _ = ctx;

        var mine = Guid.NewGuid().ToString();
        var unowned = Guid.NewGuid().ToString();
        var api = new FakeApi
        {
            UnimportedTasks =
            [
                new RemoteTask(mine, listId, "Mine", null, DateTimeOffset.UtcNow, OwnerId: "owner-1"),
                new RemoteTask(unowned, listId, "Unowned", null, DateTimeOffset.UtcNow),
            ],
        };
        var svc = BuildService(api, JwtClaimsTests.MakeToken("{\"sub\":\"owner-1\"}"));

        await svc.TickAsync(CancellationToken.None);

        Assert.NotNull(await tasks.GetByIdAsync(mine));
        Assert.NotNull(await tasks.GetByIdAsync(unowned));
    }

    // ---- already-imported task on server ----

    [Fact]
    public async Task Tick_AlreadyLocalTask_MarksImportedWithoutDuplicate()
    {
        var (listId, ctx, tasks, _) = await SeedAsync();
        using var _ = ctx;

        var existingId = Guid.NewGuid().ToString();
        var existing = new TaskEntity
        {
            Id = existingId, ListId = listId, Title = "Existing",
            Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow,
        };
        await tasks.AddAsync(existing);

        var api = new FakeApi
        {
            // Server thinks this task isn't imported yet (e.g. a retry scenario)
            UnimportedTasks = [new RemoteTask(existingId, listId, "Existing", null, DateTimeOffset.UtcNow)],
        };
        var svc = BuildService(api);
        await svc.TickAsync(CancellationToken.None);

        // Should still mark imported, and not create a duplicate
        Assert.Contains(existingId, api.MarkedImported);
        using var ctx2 = _db.CreateContext();
        var count = (await new TaskRepository(ctx2).GetByListIdAsync(listId)).Count(t => t.Id == existingId);
        Assert.Equal(1, count);
    }
}
