using System.Text.Json;
using ClaudeDo.Data;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Lifecycle;
using ClaudeDo.Worker.Tests.Infrastructure;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace ClaudeDo.Worker.Tests.Lifecycle;

public sealed class PromptFileRecoveryTests
{
    private sealed class CapturingLogger<T> : ILogger<T>
    {
        public List<string> Messages { get; } = new();

        public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
        public bool IsEnabled(LogLevel logLevel) => true;

        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
            Func<TState, Exception?, string> formatter)
            => Messages.Add(formatter(state, exception));

        private sealed class NullScope : IDisposable
        {
            public static readonly NullScope Instance = new();
            public void Dispose() { }
        }
    }

    [Fact]
    public async Task StartAsync_ReconcilesStaleDefaultAndQuarantinesOrphan_WithoutTouchingRealEdit()
    {
        var root = Path.Combine(Path.GetTempPath(), $"claudedo_prompts_{Guid.NewGuid():N}");
        try
        {
            // A file that only ever matched a now-superseded default should get reconciled away.
            Directory.CreateDirectory(root);
            const string oldDefaultText = "This used to be the bundled retry default.";
            File.WriteAllText(PromptFiles.PathFor(PromptKind.Retry, root), oldDefaultText);
            var hashes = new Dictionary<string, string>
            {
                [PromptKind.Retry.ToString()] = PromptFiles.HashOf(PromptFiles.Normalize(oldDefaultText))
            };
            File.WriteAllText(Path.Combine(root, ".defaults.json"), JsonSerializer.Serialize(hashes));

            // A genuinely edited file should survive untouched.
            PromptFiles.Save(PromptKind.System, "My real customization.", root);

            // A leftover file from a retired naming scheme should be quarantined, not deleted.
            var orphanPath = Path.Combine(root, "agent.md");
            File.WriteAllText(orphanPath, "leftover");

            var hubContext = new CapturingHubContext();
            var sut = new PromptFileRecovery(NullLogger<PromptFileRecovery>.Instance, new HubBroadcaster(hubContext), root);

            await sut.StartAsync(CancellationToken.None);

            Assert.False(File.Exists(PromptFiles.PathFor(PromptKind.Retry, root)), "Stale unedited default must be reconciled away");
            Assert.True(File.Exists(PromptFiles.PathFor(PromptKind.System, root)), "Real edit must survive");
            Assert.False(File.Exists(orphanPath), "Orphan must be moved out of the prompts root");
            Assert.True(File.Exists(Path.Combine(root, "_orphans", "agent.md")), "Orphan must be quarantined, not deleted");
            Assert.Contains(hubContext.Proxy.Calls, c =>
                c.Method == "OperationProgress" && (string)c.Args[0]! == "startup-recovery");

            await sut.StopAsync(CancellationToken.None); // must not throw
        }
        finally
        {
            if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
        }
    }

    [Fact]
    public async Task StartAsync_QuarantinesOrphanedMergeHelperExecute_WarnNamesFileAndReplacements()
    {
        var root = Path.Combine(Path.GetTempPath(), $"claudedo_prompts_{Guid.NewGuid():N}");
        try
        {
            Directory.CreateDirectory(root);
            var orphanPath = Path.Combine(root, "merge-helper-execute.md");
            File.WriteAllText(orphanPath, "my old customized triage+wait+merge prompt");

            var logger = new CapturingLogger<PromptFileRecovery>();
            var sut = new PromptFileRecovery(logger, new HubBroadcaster(new CapturingHubContext()), root);

            await sut.StartAsync(CancellationToken.None);

            var dest = Path.Combine(root, "_orphans", "merge-helper-execute.md");
            Assert.False(File.Exists(orphanPath));
            Assert.True(File.Exists(dest));

            Assert.Contains(logger.Messages, m => m.Contains("merge-helper-execute.md") && m.Contains(dest));
            Assert.Contains(logger.Messages, m =>
                m.Contains("merge-helper-wait.md") && m.Contains("merge-helper-merge.md"));
        }
        finally
        {
            if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
        }
    }
}
