// Tests for stream import cycle detection via DFS on Account.
// Go reference: accounts_test.go — TestAccountStreamImportCycles (accounts.go:1627 streamImportFormsCycle).

using NATS.Server.Auth;

namespace NATS.Server.Auth.Tests.Auth;

public class StreamImportCycleTests
{
    private static Account CreateAccount(string name) => new(name);

    /// <summary>
    /// Sets up a public stream export on <paramref name="exporter"/> for <paramref name="subject"/>
    /// and then adds a stream import on <paramref name="importer"/> from <paramref name="exporter"/>.
    /// </summary>
    private static void SetupStreamImport(Account importer, Account exporter, string subject)
    {
        exporter.AddStreamExport(subject, approved: null); // public export
        importer.AddStreamImport(exporter, subject, subject);
    }

    // 1. No cycle when the proposed source has no imports leading back to this account.
    // A imports from B; checking whether B can import from C — no path C→A exists.
    [Fact]
    public void StreamImportFormsCycle_NoCycle_ReturnsFalse()
    {
        // Go ref: accounts.go streamImportFormsCycle
        var a = CreateAccount("A");
        var b = CreateAccount("B");
        var c = CreateAccount("C");

        SetupStreamImport(a, b, "events.>"); // A imports from B
        c.AddStreamExport("other.>", approved: null);

        // B importing from C: does C→...→A exist? No.
        a.StreamImportFormsCycle(c).ShouldBeFalse();
    }

    // 2. Direct cycle: A already imports from B; proposing B imports from A = cycle.
    [Fact]
    public void StreamImportFormsCycle_DirectCycle_ReturnsTrue()
    {
        // Go ref: accounts.go streamImportFormsCycle
        var a = CreateAccount("A");
        var b = CreateAccount("B");

        SetupStreamImport(a, b, "stream.>"); // A imports from B

        // Now check: would A importing from B (again, or B's perspective) form a cycle?
        // We ask account B: does proposing A as source form a cycle?
        // i.e. b.StreamImportFormsCycle(a) — does a chain from A lead back to B?
        // A imports from B, so A→B, meaning following A's imports we reach B. Cycle confirmed.
        b.StreamImportFormsCycle(a).ShouldBeTrue();
    }

    // 3. Indirect cycle: A→B→C; proposing C import from A would create C→A→B→C.
    [Fact]
    public void StreamImportFormsCycle_IndirectCycle_ReturnsTrue()
    {
        // Go ref: accounts.go checkStreamImportsForCycles
        var a = CreateAccount("A");
        var b = CreateAccount("B");
        var c = CreateAccount("C");

        SetupStreamImport(a, b, "s.>");  // A imports from B
        SetupStreamImport(b, c, "t.>");  // B imports from C

        // Would C importing from A form a cycle? Path: A imports from B, B imports from C → cycle.
        c.StreamImportFormsCycle(a).ShouldBeTrue();
    }

    // 4. Self-import: A importing from A is always a cycle.
    [Fact]
    public void StreamImportFormsCycle_SelfImport_ReturnsTrue()
    {
        // Go ref: accounts.go streamImportFormsCycle — proposedSource == this
        var a = CreateAccount("A");

        a.StreamImportFormsCycle(a).ShouldBeTrue();
    }

    // 5. Account with no imports at all — no cycle possible.
    [Fact]
    public void StreamImportFormsCycle_NoImports_ReturnsFalse()
    {
        // Go ref: accounts.go streamImportFormsCycle — empty imports.streams
        var a = CreateAccount("A");
        var b = CreateAccount("B");

        // Neither account has any stream imports; proposing B as source for A is safe.
        a.StreamImportFormsCycle(b).ShouldBeFalse();
    }

    // 6. Diamond topology: A→B, A→C, B→D, C→D — no cycle, just shared descendant.
    [Fact]
    public void StreamImportFormsCycle_DiamondNoCycle_ReturnsFalse()
    {
        // Go ref: accounts.go checkStreamImportsForCycles — visited set prevents false positives
        var a = CreateAccount("A");
        var b = CreateAccount("B");
        var c = CreateAccount("C");
        var d = CreateAccount("D");

        SetupStreamImport(a, b, "b.>");  // A imports from B
        SetupStreamImport(a, c, "c.>");  // A imports from C
        SetupStreamImport(b, d, "d1.>"); // B imports from D
        SetupStreamImport(c, d, "d2.>"); // C imports from D

        // Proposing D import from A: does A→...→D path exist? Yes (via B and C).
        d.StreamImportFormsCycle(a).ShouldBeTrue();

        // Proposing E (new account) import from D: D has no imports, so no cycle.
        var e = CreateAccount("E");
        e.StreamImportFormsCycle(d).ShouldBeFalse();
    }

    // 7. GetStreamImportSources returns names of source accounts.
    [Fact]
    public void GetStreamImportSources_ReturnsSourceNames()
    {
        // Go ref: accounts.go imports.streams acc field
        var a = CreateAccount("A");
        var b = CreateAccount("B");
        var c = CreateAccount("C");

        SetupStreamImport(a, b, "x.>");
        SetupStreamImport(a, c, "y.>");

        var sources = a.GetStreamImportSources();

        sources.Count.ShouldBe(2);
        sources.ShouldContain("B");
        sources.ShouldContain("C");
    }

    // 8. GetStreamImportSources returns empty list when no imports exist.
    [Fact]
    public void GetStreamImportSources_Empty_ReturnsEmpty()
    {
        // Go ref: accounts.go imports.streams — empty slice
        var a = CreateAccount("A");

        var sources = a.GetStreamImportSources();

        sources.ShouldBeEmpty();
    }

    // 9. HasStreamImportFrom returns true when a matching import exists.
    [Fact]
    public void HasStreamImportFrom_True()
    {
        // Go ref: accounts.go imports.streams — acc.Name lookup
        var a = CreateAccount("A");
        var b = CreateAccount("B");

        SetupStreamImport(a, b, "events.>");

        a.HasStreamImportFrom("B").ShouldBeTrue();
    }

    // 10. HasStreamImportFrom returns false when no import from that account exists.
    [Fact]
    public void HasStreamImportFrom_False()
    {
        // Go ref: accounts.go imports.streams — acc.Name lookup miss
        var a = CreateAccount("A");
        var b = CreateAccount("B");
        var c = CreateAccount("C");

        SetupStreamImport(a, b, "events.>");

        a.HasStreamImportFrom("C").ShouldBeFalse();
        a.HasStreamImportFrom(c.Name).ShouldBeFalse();
    }
}
