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

namespace ClaudeDo.Worker.Tests.External;

public sealed class ConfigMcpToolsTests : IDisposable
{
    private readonly DbFixture _db = new();
    private readonly ClaudeDoDbContext _ctx;
    private readonly ListRepository _lists;
    private readonly TaskRepository _tasks;
    private readonly ConfigMcpTools _sut;

    public ConfigMcpToolsTests()
    {
        _ctx = _db.CreateContext();
        _lists = new ListRepository(_ctx);
        _tasks = new TaskRepository(_ctx);
        _sut = new ConfigMcpTools(_lists, _tasks, new HubBroadcaster(new CapturingHubContext()), _db.CreateFactory());
    }

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

    private async Task<string> SeedListAsync()
    {
        var id = Guid.NewGuid().ToString();
        await _lists.AddAsync(new ListEntity { Id = id, Name = "L", CreatedAt = DateTime.UtcNow });
        return id;
    }

    [Fact]
    public async Task SetAndGetListConfig_RoundTrips()
    {
        var listId = await SeedListAsync();

        var setResult = await _sut.SetListConfig(listId, "sonnet", "be terse", null, 42, cancellationToken: CancellationToken.None);
        Assert.True(setResult.Ok);
        Assert.Equal(listId, setResult.ListId);
        Assert.NotNull(setResult.Config);
        Assert.Equal("sonnet", setResult.Config!.Model);

        var cfg = await _sut.GetListConfig(listId, CancellationToken.None);

        Assert.True(cfg.Found);
        Assert.NotNull(cfg.Config);
        Assert.Equal("sonnet", cfg.Config!.Model);
        Assert.Equal("be terse", cfg.Config.SystemPrompt);
        Assert.Null(cfg.Config.AgentPath);
        Assert.Equal(42, cfg.Config.MaxTurns);
    }

    [Fact]
    public async Task GetListConfig_NoConfigSet_ReturnsNotFound()
    {
        var listId = await SeedListAsync();

        var cfg = await _sut.GetListConfig(listId, CancellationToken.None);

        Assert.False(cfg.Found);
        Assert.Null(cfg.Config);
    }

    [Fact]
    public async Task SetListConfig_PartialUpdate_LeavesOtherFieldsUntouched()
    {
        var listId = await SeedListAsync();
        await _sut.SetListConfig(listId, "opus", "be terse", "agent.md", 30, cancellationToken: CancellationToken.None);

        var result = await _sut.SetListConfig(listId, maxTurns: 40, cancellationToken: CancellationToken.None);

        Assert.Equal("opus", result.Config!.Model);
        Assert.Equal("be terse", result.Config.SystemPrompt);
        Assert.Equal("agent.md", result.Config.AgentPath);
        Assert.Equal(40, result.Config.MaxTurns);
    }

    [Fact]
    public async Task SetListConfig_ClearFields_ClearsOnlyNamedField()
    {
        var listId = await SeedListAsync();
        await _sut.SetListConfig(listId, "opus", "be terse", "agent.md", 30, cancellationToken: CancellationToken.None);

        var result = await _sut.SetListConfig(listId, clearFields: new[] { "model" }, cancellationToken: CancellationToken.None);

        Assert.Null(result.Config!.Model);
        Assert.Equal("be terse", result.Config.SystemPrompt);
        Assert.Equal("agent.md", result.Config.AgentPath);
        Assert.Equal(30, result.Config.MaxTurns);
    }

    [Fact]
    public async Task SetListConfig_AllFieldsCleared_DeletesConfig()
    {
        var listId = await SeedListAsync();
        await _sut.SetListConfig(listId, "sonnet", null, null, null, cancellationToken: CancellationToken.None);

        var clearResult = await _sut.SetListConfig(
            listId, clearFields: new[] { "model", "systemPrompt", "agentPath", "maxTurns" }, cancellationToken: CancellationToken.None);

        Assert.True(clearResult.Ok);
        Assert.Null(clearResult.Config);
        var cfg = await _sut.GetListConfig(listId, CancellationToken.None);
        Assert.False(cfg.Found);
    }

    [Fact]
    public async Task SetListConfig_UnknownClearField_Throws()
    {
        var listId = await SeedListAsync();

        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            _sut.SetListConfig(listId, clearFields: new[] { "bogus" }, cancellationToken: CancellationToken.None));
    }

    [Fact]
    public async Task SetListConfig_SetsVerifyCommand()
    {
        var listId = await SeedListAsync();

        var result = await _sut.SetListConfig(listId, verifyCommand: "dotnet test", cancellationToken: CancellationToken.None);

        Assert.Equal("dotnet test", result.Config!.VerifyCommand);
        var cfg = await _sut.GetListConfig(listId, CancellationToken.None);
        Assert.Equal("dotnet test", cfg.Config!.VerifyCommand);
    }

    [Fact]
    public async Task SetListConfig_OverwritesVerifyCommand()
    {
        var listId = await SeedListAsync();
        await _sut.SetListConfig(listId, verifyCommand: "dotnet test", cancellationToken: CancellationToken.None);

        var result = await _sut.SetListConfig(listId, verifyCommand: "dotnet build", cancellationToken: CancellationToken.None);

        Assert.Equal("dotnet build", result.Config!.VerifyCommand);
    }

    [Fact]
    public async Task SetListConfig_ClearVerifyCommand_ClearsOnlyThatField()
    {
        var listId = await SeedListAsync();
        await _sut.SetListConfig(listId, "opus", verifyCommand: "dotnet test", cancellationToken: CancellationToken.None);

        var result = await _sut.SetListConfig(listId, clearFields: new[] { "verifyCommand" }, cancellationToken: CancellationToken.None);

        Assert.Equal("opus", result.Config!.Model);
        Assert.Null(result.Config.VerifyCommand);
    }

    [Fact]
    public async Task SetListConfig_ConfigWithOnlyVerifyCommand_IsCreated()
    {
        var listId = await SeedListAsync();

        var result = await _sut.SetListConfig(listId, verifyCommand: "dotnet test", cancellationToken: CancellationToken.None);

        Assert.True(result.Ok);
        Assert.NotNull(result.Config);
        Assert.Equal("dotnet test", result.Config!.VerifyCommand);
        Assert.Null(result.Config.Model);
    }

    [Fact]
    public async Task SetListConfig_ClearingEveryOtherField_KeepsConfigWhenVerifyCommandRemains()
    {
        var listId = await SeedListAsync();
        await _sut.SetListConfig(listId, "sonnet", "be terse", "agent.md", 30, "dotnet test", cancellationToken: CancellationToken.None);

        var result = await _sut.SetListConfig(
            listId, clearFields: new[] { "model", "systemPrompt", "agentPath", "maxTurns" }, cancellationToken: CancellationToken.None);

        Assert.True(result.Ok);
        Assert.NotNull(result.Config);
        Assert.Equal("dotnet test", result.Config!.VerifyCommand);
        Assert.Null(result.Config.Model);
    }

    [Fact]
    public async Task SetListConfig_ClearingEveryFieldIncludingVerifyCommand_DeletesConfig()
    {
        var listId = await SeedListAsync();
        await _sut.SetListConfig(listId, "sonnet", verifyCommand: "dotnet test", cancellationToken: CancellationToken.None);

        var result = await _sut.SetListConfig(
            listId,
            clearFields: new[] { "model", "systemPrompt", "agentPath", "maxTurns", "verifyCommand" },
            cancellationToken: CancellationToken.None);

        Assert.True(result.Ok);
        Assert.Null(result.Config);
        Assert.Null(await _lists.GetConfigAsync(listId));
    }

    [Fact]
    public async Task SetTaskConfig_PersistsOverrides()
    {
        var listId = await SeedListAsync();
        var task = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = listId,
            Title = "t",
            Status = ClaudeDo.Data.Models.TaskStatus.Idle,
            CreatedAt = DateTime.UtcNow,
            CommitType = "chore",
        };
        await _tasks.AddAsync(task);

        var result = await _sut.SetTaskConfig(task.Id, "opus", null, null, 15, cancellationToken: CancellationToken.None);

        Assert.True(result.Ok);
        Assert.Equal(task.Id, result.TaskId);
        Assert.Equal("opus", result.Config!.Model);
        Assert.Equal(15, result.Config.MaxTurns);

        var loaded = await _tasks.GetByIdAsync(task.Id);
        Assert.Equal("opus", loaded!.Model);
        Assert.Equal(15, loaded.MaxTurns);
    }

    [Fact]
    public async Task SetTaskConfig_PartialUpdate_LeavesOtherOverridesUntouched()
    {
        var listId = await SeedListAsync();
        var task = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = listId,
            Title = "t",
            Status = ClaudeDo.Data.Models.TaskStatus.Idle,
            CreatedAt = DateTime.UtcNow,
            CommitType = "chore",
        };
        await _tasks.AddAsync(task);
        await _sut.SetTaskConfig(task.Id, "opus", "be terse", "agent.md", 15, cancellationToken: CancellationToken.None);

        var result = await _sut.SetTaskConfig(task.Id, maxTurns: 40, cancellationToken: CancellationToken.None);

        Assert.True(result.Ok);
        Assert.Equal("opus", result.Config!.Model);
        Assert.Equal("be terse", result.Config.SystemPrompt);
        Assert.Equal("agent.md", result.Config.AgentPath);
        Assert.Equal(40, result.Config.MaxTurns);

        var loaded = await _tasks.GetByIdAsync(task.Id);
        Assert.Equal("opus", loaded!.Model);
        Assert.Equal("be terse", loaded.SystemPrompt);
        Assert.Equal(40, loaded.MaxTurns);
    }

    [Fact]
    public async Task SetTaskConfig_ClearFields_ClearsOnlyNamedOverride()
    {
        var listId = await SeedListAsync();
        var task = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = listId,
            Title = "t",
            Status = ClaudeDo.Data.Models.TaskStatus.Idle,
            CreatedAt = DateTime.UtcNow,
            CommitType = "chore",
        };
        await _tasks.AddAsync(task);
        await _sut.SetTaskConfig(task.Id, "opus", "be terse", "agent.md", 15, cancellationToken: CancellationToken.None);

        var result = await _sut.SetTaskConfig(task.Id, clearFields: new[] { "systemPrompt" }, cancellationToken: CancellationToken.None);

        Assert.True(result.Ok);
        Assert.Equal("opus", result.Config!.Model);
        Assert.Null(result.Config.SystemPrompt);
        Assert.Equal("agent.md", result.Config.AgentPath);
        Assert.Equal(15, result.Config.MaxTurns);

        var loaded = await _tasks.GetByIdAsync(task.Id);
        Assert.Equal("opus", loaded!.Model);
        Assert.Null(loaded.SystemPrompt);
        Assert.Equal("agent.md", loaded.AgentPath);
        Assert.Equal(15, loaded.MaxTurns);
    }

    [Fact]
    public async Task SetTaskConfig_UnknownClearField_Throws()
    {
        var listId = await SeedListAsync();
        var task = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = listId,
            Title = "t",
            Status = ClaudeDo.Data.Models.TaskStatus.Idle,
            CreatedAt = DateTime.UtcNow,
            CommitType = "chore",
        };
        await _tasks.AddAsync(task);

        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            _sut.SetTaskConfig(task.Id, clearFields: new[] { "bogus" }, cancellationToken: CancellationToken.None));
    }

    [Fact]
    public async Task SetListConfig_PreservesSettingsItDoesNotExpose()
    {
        var listId = await SeedListAsync();
        await _lists.SetConfigAsync(new ListConfigEntity
        {
            ListId = listId,
            Model = "sonnet",
            SessionSkills = "[\"superpowers\"]",
            VerifyCommand = "dotnet build",
            PermissionMode = "acceptEdits",
        });

        await _sut.SetListConfig(listId, model: "opus", cancellationToken: CancellationToken.None);

        var cfg = await _lists.GetConfigAsync(listId);
        Assert.NotNull(cfg);
        Assert.Equal("opus", cfg!.Model);
        Assert.Equal("[\"superpowers\"]", cfg.SessionSkills);
        Assert.Equal("dotnet build", cfg.VerifyCommand);
        // SetConfigAsync copies the entity verbatim, so a field this tool doesn't expose would
        // reset to its default unless it is carried over explicitly.
        Assert.Equal("acceptEdits", cfg.PermissionMode);
    }

    [Fact]
    public async Task SetListConfig_ClearingEveryExposedField_KeepsRowWhenOtherSettingsRemain()
    {
        var listId = await SeedListAsync();
        await _lists.SetConfigAsync(new ListConfigEntity
        {
            ListId = listId,
            Model = "sonnet",
            PermissionMode = "acceptEdits",
        });

        var result = await _sut.SetListConfig(
            listId,
            clearFields: new[] { "model", "systemPrompt", "agentPath", "maxTurns", "verifyCommand" },
            cancellationToken: CancellationToken.None);

        Assert.True(result.Ok);
        Assert.Null(result.Config);

        var cfg = await _lists.GetConfigAsync(listId);
        Assert.NotNull(cfg);
        Assert.Null(cfg!.Model);
        Assert.Equal("acceptEdits", cfg.PermissionMode);
    }

    [Fact]
    public async Task SetListConfig_ClearingEveryField_DeletesRowWhenNothingElseIsSet()
    {
        var listId = await SeedListAsync();
        await _sut.SetListConfig(listId, "sonnet", cancellationToken: CancellationToken.None);

        await _sut.SetListConfig(
            listId,
            clearFields: new[] { "model", "systemPrompt", "agentPath", "maxTurns" },
            cancellationToken: CancellationToken.None);

        Assert.Null(await _lists.GetConfigAsync(listId));
    }

    [Fact]
    public async Task GetTaskConfig_NoOverrideSet_ReturnsNotFound()
    {
        var listId = await SeedListAsync();
        var task = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = listId,
            Title = "t",
            Status = ClaudeDo.Data.Models.TaskStatus.Idle,
            CreatedAt = DateTime.UtcNow,
            CommitType = "chore",
        };
        await _tasks.AddAsync(task);

        var cfg = await _sut.GetTaskConfig(task.Id, CancellationToken.None);

        Assert.False(cfg.Found);
        Assert.Null(cfg.Config);
    }
}
