using System;
using System.IO;
using System.Threading.Tasks;
using TotalRecall.Cli.Commands.Memory;
using TotalRecall.Cli.Tests.TestSupport;
using TotalRecall.Core;
using Xunit;

namespace TotalRecall.Cli.Tests.Commands.Memory;

[Collection("ConsoleCapture")]
public sealed class PromoteCommandTests : IDisposable
{
    private readonly TextWriter _origOut;
    private readonly TextWriter _origErr;
    private readonly StringWriter _outWriter = new();
    private readonly StringWriter _errWriter = new();

    public PromoteCommandTests()
    {
        _origOut = Console.Out;
        _origErr = Console.Error;
        Console.SetOut(_outWriter);
        Console.SetError(_errWriter);
    }

    public void Dispose()
    {
        Console.SetOut(_origOut);
        Console.SetError(_origErr);
    }

    private static (PromoteCommand cmd, FakeStore store, FakeVectorSearch vec, RecordingEmbedder emb) Build()
    {
        var store = new FakeStore();
        var vec = new FakeVectorSearch();
        var emb = new RecordingEmbedder();
        return (new PromoteCommand(store, vec, emb), store, vec, emb);
    }

    [Fact]
    public async Task MissingId_ReturnsExit2()
    {
        var (cmd, _, _, _) = Build();
        var code = await cmd.RunAsync(Array.Empty<string>());
        Assert.Equal(2, code);
        Assert.Contains("<id>", _errWriter.ToString());
    }

    [Fact]
    public async Task EntryNotFound_ReturnsExit1()
    {
        var (cmd, _, _, _) = Build();
        var code = await cmd.RunAsync(new[] { "nope" });
        Assert.Equal(1, code);
        Assert.Contains("not found", _errWriter.ToString());
    }

    [Fact]
    public async Task HappyPath_ColdMemoryToWarm_MovesAndReEmbeds()
    {
        var (cmd, store, vec, emb) = Build();
        store.Seed(Tier.Cold, ContentType.Memory, EntryFactory.Make("abc", "body"));

        var code = await cmd.RunAsync(new[] { "abc", "--tier", "warm" });

        Assert.Equal(0, code);
        Assert.Single(vec.Deletes);
        // abc is seeded first → synthetic rowid 1 in FakeMemoryInfra.FakeStore.
        Assert.Equal((Tier.Cold, ContentType.Memory, 1L), vec.Deletes[0]);
        Assert.Single(store.MoveCalls);
        Assert.Equal((Tier.Cold, ContentType.Memory, Tier.Warm, ContentType.Memory, "abc"), store.MoveCalls[0]);
        Assert.Single(emb.Calls);
        Assert.Equal("body", emb.Calls[0]);
        Assert.Single(vec.Inserts);
        Assert.Equal(Tier.Warm, vec.Inserts[0].Tier);
        Assert.Equal(ContentType.Memory, vec.Inserts[0].Type);
        Assert.Equal("abc", vec.Inserts[0].Id);
        Assert.Contains("promoted abc from cold/memory to warm/memory", _outWriter.ToString());
    }

    [Fact]
    public async Task HappyPath_DefaultsToHot()
    {
        var (cmd, store, _, _) = Build();
        store.Seed(Tier.Warm, ContentType.Knowledge, EntryFactory.Make("k1"));
        var code = await cmd.RunAsync(new[] { "k1" });
        Assert.Equal(0, code);
        Assert.Equal((Tier.Warm, ContentType.Knowledge, Tier.Hot, ContentType.Knowledge, "k1"), store.MoveCalls[0]);
    }

    [Fact]
    public async Task InvalidDirection_HotToCold_ReturnsExit2()
    {
        var (cmd, store, _, _) = Build();
        store.Seed(Tier.Hot, ContentType.Memory, EntryFactory.Make("abc"));
        var code = await cmd.RunAsync(new[] { "abc", "--tier", "hot" });
        // hot -> hot is not strictly warmer
        Assert.Equal(2, code);
        Assert.Contains("target must be warmer", _errWriter.ToString());
    }

    [Fact]
    public async Task PromoteToCold_ReturnsExit2()
    {
        var (cmd, _, _, _) = Build();
        var code = await cmd.RunAsync(new[] { "abc", "--tier", "cold" });
        Assert.Equal(2, code);
        Assert.Contains("cannot promote to cold", _errWriter.ToString());
    }

    [Fact]
    public async Task BadTier_ReturnsExit2()
    {
        var (cmd, _, _, _) = Build();
        var code = await cmd.RunAsync(new[] { "abc", "--tier", "nope" });
        Assert.Equal(2, code);
        Assert.Contains("invalid --tier", _errWriter.ToString());
    }

    // ---------------- Task 3: pinned-tier guards ----------------

    [Fact]
    public async Task PinnedSource_ReturnsExit2_MentionsUnpin()
    {
        // Promoting an entry that lives in the pinned tier must be refused;
        // the user should use memory_unpin / 'memory unpin' instead.
        var (cmd, store, _, _) = Build();
        store.Seed(Tier.Pinned, ContentType.Memory, EntryFactory.Make("p1"));

        var code = await cmd.RunAsync(new[] { "p1" });

        Assert.Equal(2, code);
        Assert.Contains("unpin", _errWriter.ToString(), StringComparison.OrdinalIgnoreCase);
        Assert.Empty(store.MoveCalls); // nothing moved
    }

    [Fact]
    public async Task PinnedTarget_ReturnsExit2_MentionsPin()
    {
        // Passing --tier pinned must be rejected before any store lookup;
        // the user should use memory_pin / 'memory pin' instead.
        var (cmd, _, _, _) = Build();
        var code = await cmd.RunAsync(new[] { "abc", "--tier", "pinned" });

        Assert.Equal(2, code);
        Assert.Contains("pin", _errWriter.ToString(), StringComparison.OrdinalIgnoreCase);
    }
}
