using DragonISO.Engine.Discovery;
using DragonISO.Engine.Domain;

namespace DragonISO.Engine.Tests.Discovery;

public class ParticipantTrackerTests
{
    private static NdiSource Source(string machine, string display) =>
        new($"{machine} (Teams - {display})", machine, NdiSourceKind.Participant, display);

    private static DateTimeOffset T0 => DateTimeOffset.UnixEpoch;

    [Fact]
    public void Added_CreatesParticipant()
    {
        var tracker = new ParticipantTracker(renameWindow: TimeSpan.FromSeconds(5), now: () => T0);
        tracker.Apply(new DiscoveryEvent.Added(Source("PC1", "Jane")));

        tracker.Participants.Should().HaveCount(1);
        var p = tracker.Participants[0];
        p.DisplayName.Should().Be("Jane");
        p.CurrentSource!.MachineName.Should().Be("PC1");
    }

    [Fact]
    public void Removed_NullsCurrentSourceButKeepsParticipant()
    {
        var time = T0;
        var tracker = new ParticipantTracker(TimeSpan.FromSeconds(5), () => time);
        tracker.Apply(new DiscoveryEvent.Added(Source("PC1", "Jane")));

        time = T0.AddSeconds(1);
        tracker.Apply(new DiscoveryEvent.Removed(Source("PC1", "Jane")));

        tracker.Participants.Should().HaveCount(1);
        tracker.Participants[0].CurrentSource.Should().BeNull();
    }

    [Fact]
    public void RenameWithinWindow_TransfersParticipantId()
    {
        var time = T0;
        var tracker = new ParticipantTracker(TimeSpan.FromSeconds(5), () => time);
        tracker.Apply(new DiscoveryEvent.Added(Source("PC1", "Jane")));
        var originalId = tracker.Participants[0].Id;

        time = T0.AddSeconds(1);
        tracker.Apply(new DiscoveryEvent.Removed(Source("PC1", "Jane")));

        time = T0.AddSeconds(3);
        tracker.Apply(new DiscoveryEvent.Added(Source("PC1", "Jane (PM)")));

        tracker.Participants.Should().HaveCount(1);
        tracker.Participants[0].Id.Should().Be(originalId);
        tracker.Participants[0].DisplayName.Should().Be("Jane (PM)");
    }

    [Fact]
    public void RenameAfterWindow_TreatsAsNewParticipant()
    {
        var time = T0;
        var tracker = new ParticipantTracker(TimeSpan.FromSeconds(5), () => time);
        tracker.Apply(new DiscoveryEvent.Added(Source("PC1", "Jane")));
        var originalId = tracker.Participants[0].Id;

        time = T0.AddSeconds(1);
        tracker.Apply(new DiscoveryEvent.Removed(Source("PC1", "Jane")));

        time = T0.AddSeconds(20);
        tracker.Apply(new DiscoveryEvent.Added(Source("PC1", "Bob")));

        tracker.Participants.Should().HaveCount(2);
        tracker.Participants.Should().Contain(p => p.Id == originalId);
        tracker.Participants.Should().Contain(p => p.DisplayName == "Bob" && p.Id != originalId);
    }

    [Fact]
    public void DifferentMachine_DoesNotInheritIdentity()
    {
        var time = T0;
        var tracker = new ParticipantTracker(TimeSpan.FromSeconds(5), () => time);
        tracker.Apply(new DiscoveryEvent.Added(Source("PC1", "Jane")));

        time = T0.AddSeconds(1);
        tracker.Apply(new DiscoveryEvent.Removed(Source("PC1", "Jane")));

        time = T0.AddSeconds(2);
        tracker.Apply(new DiscoveryEvent.Added(Source("PC2", "Jane (PM)")));

        tracker.Participants.Should().HaveCount(2);
    }

    [Fact]
    public void NonParticipantSources_AreIgnored()
    {
        var tracker = new ParticipantTracker(TimeSpan.FromSeconds(5), () => T0);
        var screen = new NdiSource("PC1 (Teams Screen Share)", "PC1", NdiSourceKind.ScreenShare, null);

        tracker.Apply(new DiscoveryEvent.Added(screen));

        tracker.Participants.Should().BeEmpty();
    }

    [Fact]
    public void ActiveSpeaker_AppearsAsSyntheticAutoMixParticipant()
    {
        // The ActiveSpeaker kind (legacy "MACHINE (Teams)" or current
        // "MACHINE (MS Teams - Active Speaker)") is now surfaced as a row in the
        // participant list with a synthetic display name "Active Speaker", so the
        // operator can route it to its own normalized ISO via the same toggle as
        // any participant.
        var tracker = new ParticipantTracker(TimeSpan.FromSeconds(5), () => T0);
        var autoMix = new NdiSource("WOOGLIN (MS Teams - Active Speaker)", "WOOGLIN", NdiSourceKind.ActiveSpeaker, null);

        tracker.Apply(new DiscoveryEvent.Added(autoMix));

        tracker.Participants.Should().HaveCount(1);
        tracker.Participants[0].DisplayName.Should().Be("Active Speaker");
        tracker.Participants[0].CurrentSource!.FullName.Should().Be(autoMix.FullName);
    }

    [Fact]
    public void ActiveSpeaker_ReAddOnSameMachine_PreservesId()
    {
        // The synthetic auto-mix participant must use a deterministic Id (derived
        // from "auto-mix:<machine>") so a discovery cycle that re-adds the source
        // doesn't duplicate the row and the operator's ISO assignment stays bound.
        var tracker = new ParticipantTracker(TimeSpan.FromSeconds(5), () => T0);
        var autoMix = new NdiSource("WOOGLIN (MS Teams - Active Speaker)", "WOOGLIN", NdiSourceKind.ActiveSpeaker, null);

        tracker.Apply(new DiscoveryEvent.Added(autoMix));
        var firstId = tracker.Participants[0].Id;
        tracker.Apply(new DiscoveryEvent.Added(autoMix));

        tracker.Participants.Should().HaveCount(1);
        tracker.Participants[0].Id.Should().Be(firstId);
    }

    [Fact]
    public void Added_SameSourceTwice_IsIdempotent_DoesNotDuplicate()
    {
        // Regression for the discovery-refresh path: when the operator clicks "Refresh"
        // we deliberately re-emit Added events for sources that were already known
        // (clearing the discovery service's seen-set so it re-fires everything coming
        // back from the rebuilt finder). The tracker must coalesce these â€” minting a
        // new Guid would orphan the operator's running ISO and visibly duplicate the
        // row in the participants list.
        var time = T0;
        var tracker = new ParticipantTracker(TimeSpan.FromSeconds(5), () => time);
        var jane = Source("PC1", "Jane");

        tracker.Apply(new DiscoveryEvent.Added(jane));
        var originalId = tracker.Participants[0].Id;

        time = T0.AddSeconds(2);
        tracker.Apply(new DiscoveryEvent.Added(jane));

        tracker.Participants.Should().HaveCount(1);
        tracker.Participants[0].Id.Should().Be(originalId,
            because: "re-emitting Added for the still-live source must not mint a fresh Id");
        tracker.Participants[0].LastSeen.Should().Be(time,
            because: "the second Add should refresh LastSeen");
    }

    [Fact]
    public void ActiveSpeakerRemove_DoesNotPoisonRenameWindowForLaterParticipant()
    {
        // Regression: the rename-window heuristic matches by MachineName alone, so a
        // disappearing ActiveSpeaker source on a machine could cause the next
        // Participant joining that same machine to inherit the auto-mix's GUID, AND
        // the auto-mix row would be renamed to the participant's display name.
        // HandleAutoMixRemoved deliberately skips _recentlyRemoved.
        var tracker = new ParticipantTracker(TimeSpan.FromSeconds(5), () => T0);
        var autoMix = new NdiSource("WOOGLIN (MS Teams - Active Speaker)", "WOOGLIN", NdiSourceKind.ActiveSpeaker, null);
        var jane = new NdiSource("WOOGLIN (MS Teams - Jane)", "WOOGLIN", NdiSourceKind.Participant, "Jane");

        tracker.Apply(new DiscoveryEvent.Added(autoMix));
        var autoMixId = tracker.Participants[0].Id;
        tracker.Apply(new DiscoveryEvent.Removed(autoMix));
        tracker.Apply(new DiscoveryEvent.Added(jane));

        // Two distinct rows: the auto-mix (offline, no source) and Jane (a brand-new participant).
        tracker.Participants.Should().HaveCount(2);
        var jp = tracker.Participants.Single(p => p.DisplayName == "Jane");
        jp.Id.Should().NotBe(autoMixId,
            because: "Jane is a fresh Participant, not a renamed auto-mix");
        var asRow = tracker.Participants.Single(p => p.DisplayName == "Active Speaker");
        asRow.Id.Should().Be(autoMixId);
        asRow.CurrentSource.Should().BeNull(
            because: "the auto-mix source went away and hasn't been re-added");
    }
}
