using ClaudeDo.Worker.Skills;
using ClaudeDo.Worker.Tests.Infrastructure;
using Xunit;

namespace ClaudeDo.Worker.Tests.Skills;

public sealed class SessionSkillRegistryTests : IDisposable
{
    private readonly DbFixture _db = new();
    private readonly string _skillsRoot;
    private readonly string _workDir;
    private readonly FakeRepoCloner _cloner = new();

    public SessionSkillRegistryTests()
    {
        _workDir = Path.Combine(Path.GetTempPath(), $"claudedo_skill_test_{Guid.NewGuid():N}");
        _skillsRoot = Path.Combine(_workDir, "session-skills");
        Directory.CreateDirectory(_skillsRoot);
    }

    public void Dispose()
    {
        _db.Dispose();
        try { Directory.Delete(_workDir, recursive: true); } catch { /* best effort */ }
    }

    private SessionSkillRegistry CreateRegistry() =>
        new(_db.CreateFactory(), _cloner, _skillsRoot);

    private string NewFixtureDir([System.Runtime.CompilerServices.CallerMemberName] string name = "") =>
        Path.Combine(_workDir, "fixtures", $"{name}_{Guid.NewGuid():N}");

    [Fact]
    public async Task InstallAsync_multi_skill_layout_creates_one_row_and_dir_per_skill()
    {
        var fixtureDir = SkillFixtureBuilder.MultiSkillRepo(NewFixtureDir());
        _cloner.Register("https://example.com/multi.git", fixtureDir, "sha-multi");
        var registry = CreateRegistry();

        var installed = await registry.InstallAsync("https://example.com/multi.git", default);

        Assert.Equal(new[] { "ponytail", "ponytail-help" }, installed.OrderBy(x => x).ToArray());

        var rows = await registry.ListAsync(default);
        Assert.Equal(2, rows.Count);

        var ponytail = rows.Single(r => r.Name == "ponytail");
        Assert.Equal("https://example.com/multi.git", ponytail.SourceUrl);
        Assert.Equal("sha-multi", ponytail.PinnedRef);
        Assert.Equal("skills/ponytail", ponytail.Subpath);
        Assert.Equal("Ties hair into a ponytail. Works on long hair only.", ponytail.Description);
        Assert.True(File.Exists(Path.Combine(_skillsRoot, "ponytail", "SKILL.md")));

        var help = rows.Single(r => r.Name == "ponytail-help");
        Assert.Equal("skills/ponytail-help", help.Subpath);
        Assert.Equal("Help text for the ponytail skill.", help.Description);
        Assert.True(File.Exists(Path.Combine(_skillsRoot, "ponytail-help", "SKILL.md")));
    }

    [Fact]
    public async Task InstallAsync_root_skill_md_creates_single_row_with_dot_subpath()
    {
        var fixtureDir = SkillFixtureBuilder.RootSkillRepo(NewFixtureDir());
        _cloner.Register("https://example.com/solo.git", fixtureDir, "sha-solo");
        var registry = CreateRegistry();

        var installed = await registry.InstallAsync("https://example.com/solo.git", default);

        Assert.Equal(new[] { "solo-skill" }, installed);

        var rows = await registry.ListAsync(default);
        var row = Assert.Single(rows);
        Assert.Equal("solo-skill", row.Name);
        Assert.Equal(".", row.Subpath);
        Assert.Equal("A single root-level skill.", row.Description);
        Assert.True(File.Exists(Path.Combine(_skillsRoot, "solo-skill", "SKILL.md")));
    }

    [Fact]
    public async Task InstallAsync_no_skills_found_throws()
    {
        var fixtureDir = SkillFixtureBuilder.EmptyRepo(NewFixtureDir());
        _cloner.Register("https://example.com/empty.git", fixtureDir, "sha-empty");
        var registry = CreateRegistry();

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => registry.InstallAsync("https://example.com/empty.git", default));
    }

    [Fact]
    public async Task InstallAsync_cross_source_name_collision_throws()
    {
        var fixtureA = SkillFixtureBuilder.RootSkillRepo(NewFixtureDir());
        _cloner.Register("https://example.com/a.git", fixtureA, "sha-a");
        var registry = CreateRegistry();
        await registry.InstallAsync("https://example.com/a.git", default);

        // Different source URL, same skill name ("solo-skill").
        var fixtureB = SkillFixtureBuilder.RootSkillRepo(NewFixtureDir());
        _cloner.Register("https://example.com/b.git", fixtureB, "sha-b");

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => registry.InstallAsync("https://example.com/b.git", default));

        // Original row must be untouched.
        var rows = await registry.ListAsync(default);
        var row = Assert.Single(rows);
        Assert.Equal("https://example.com/a.git", row.SourceUrl);
        Assert.Equal("sha-a", row.PinnedRef);
    }

    [Fact]
    public async Task InstallAsync_reinstalling_same_url_refreshes_pinned_ref_without_collision()
    {
        var fixtureDir = SkillFixtureBuilder.RootSkillRepo(NewFixtureDir());
        _cloner.Register("https://example.com/solo.git", fixtureDir, "sha-1");
        var registry = CreateRegistry();
        await registry.InstallAsync("https://example.com/solo.git", default);

        _cloner.Register("https://example.com/solo.git", fixtureDir, "sha-2");
        var installed = await registry.InstallAsync("https://example.com/solo.git", default);

        Assert.Equal(new[] { "solo-skill" }, installed);
        var rows = await registry.ListAsync(default);
        var row = Assert.Single(rows);
        Assert.Equal("sha-2", row.PinnedRef);
    }

    [Fact]
    public async Task RemoveAsync_deletes_all_dirs_and_rows_for_source()
    {
        var fixtureDir = SkillFixtureBuilder.MultiSkillRepo(NewFixtureDir());
        _cloner.Register("https://example.com/multi.git", fixtureDir, "sha-multi");
        var registry = CreateRegistry();
        await registry.InstallAsync("https://example.com/multi.git", default);

        await registry.RemoveAsync("https://example.com/multi.git", default);

        Assert.Empty(await registry.ListAsync(default));
        Assert.False(Directory.Exists(Path.Combine(_skillsRoot, "ponytail")));
        Assert.False(Directory.Exists(Path.Combine(_skillsRoot, "ponytail-help")));
    }

    [Fact]
    public async Task UpdateAsync_removes_stale_skill_when_source_drops_it()
    {
        var multiFixture = SkillFixtureBuilder.MultiSkillRepo(NewFixtureDir());
        _cloner.Register("https://example.com/multi.git", multiFixture, "sha-1");
        var registry = CreateRegistry();
        await registry.InstallAsync("https://example.com/multi.git", default);

        // Source now only provides "ponytail" (drops "ponytail-help").
        var reducedFixtureDir = NewFixtureDir();
        Directory.CreateDirectory(Path.Combine(reducedFixtureDir, "skills"));
        CopyDir(Path.Combine(multiFixture, "skills", "ponytail"), Path.Combine(reducedFixtureDir, "skills", "ponytail"));
        _cloner.Register("https://example.com/multi.git", reducedFixtureDir, "sha-2");

        await registry.UpdateAsync("https://example.com/multi.git", default);

        var rows = await registry.ListAsync(default);
        var row = Assert.Single(rows);
        Assert.Equal("ponytail", row.Name);
        Assert.Equal("sha-2", row.PinnedRef);
        Assert.True(Directory.Exists(Path.Combine(_skillsRoot, "ponytail")));
        Assert.False(Directory.Exists(Path.Combine(_skillsRoot, "ponytail-help")));
    }

    private static void CopyDir(string sourceDir, string destDir)
    {
        Directory.CreateDirectory(destDir);
        foreach (var file in Directory.EnumerateFiles(sourceDir, "*", SearchOption.AllDirectories))
        {
            var relative = Path.GetRelativePath(sourceDir, file);
            var destPath = Path.Combine(destDir, relative);
            var destParent = Path.GetDirectoryName(destPath);
            if (!string.IsNullOrEmpty(destParent))
                Directory.CreateDirectory(destParent);
            File.Copy(file, destPath, overwrite: true);
        }
    }
}
