using System.IO;
using FluentAssertions;
using DragonISO.App.Services;
using DragonISO.App.Tests.Fakes;
using DragonISO.App.ViewModels;
using DragonISO.Engine.Domain;

namespace DragonISO.App.Tests.Services;

// PresetApplier reconciles a saved preset's per-display-name assignments
// against the live participant view-model list. Tests pin the four
// transitions (enableâ†’stay, disableâ†’stay, offâ†’enable, onâ†’disable) plus
// the partial-meeting path where the preset references participants
// who aren't currently present.
//
// We share a collection with OperatorPresetStoreTests because both
// classes mutate OperatorPresetStore.PathOverride; xUnit's default
// parallelism would otherwise let one class clobber the other's path
// mid-run.
[Collection(PresetStoreCollection.Name)]
public sealed class PresetApplierTests : IDisposable
{
    private readonly string _tempPresets;
    private readonly string? _previousPresetOverride;

    public PresetApplierTests()
    {
        _tempPresets = Path.Combine(Path.GetTempPath(), $"Dragon-ISO-presets-{Guid.NewGuid():N}.json");
        _previousPresetOverride = OperatorPresetStore.PathOverride;
        OperatorPresetStore.PathOverride = _tempPresets;
    }

    public void Dispose()
    {
        OperatorPresetStore.PathOverride = _previousPresetOverride;
        try { if (File.Exists(_tempPresets)) File.Delete(_tempPresets); }
        catch { /* cleanup best-effort */ }
    }

    private static ParticipantViewModel MakeParticipant(
        StubIsoController controller, string displayName, bool isEnabled = false)
    {
        var participant = new Participant(
            Id: Guid.NewGuid(),
            DisplayName: displayName,
            CurrentSource: null,
            FirstSeen: DateTimeOffset.UtcNow,
            LastSeen: DateTimeOffset.UtcNow);
        return new ParticipantViewModel(controller, participant) { IsEnabled = isEnabled };
    }

    private static OperatorPresetStore.Preset Preset(params (string Name, bool Enabled, string? Custom)[] rows) =>
        new(
            Name: "test-preset",
            SavedAt: DateTimeOffset.UtcNow,
            Assignments: rows.Select(r =>
                new OperatorPresetStore.Assignment(r.Name, r.Custom, r.Enabled)).ToList());

    [Fact]
    public async Task Apply_EnablesParticipantsThatPresetSaysEnabled_AndAreCurrentlyOff()
    {
        var controller = new StubIsoController();
        var alice = MakeParticipant(controller, "Alice", isEnabled: false);
        var bob   = MakeParticipant(controller, "Bob",   isEnabled: false);
        var preset = Preset(("Alice", true, "ALICE_OUT"), ("Bob", true, null));

        var result = await PresetApplier.ApplyAsync(preset, new[] { alice, bob }, controller, dispatcher: null);

        result.Matched.Should().Be(2);
        result.Changed.Should().Be(2);
        result.Skipped.Should().Be(0);

        controller.EnableCalls.Should().HaveCount(2);
        controller.EnableCalls.Should().Contain(c => c.Id == alice.Id && c.Name == "ALICE_OUT");
        controller.EnableCalls.Should().Contain(c => c.Id == bob.Id   && c.Name == null);

        alice.IsEnabled.Should().BeTrue();
        bob.IsEnabled.Should().BeTrue();
        alice.CustomName.Should().Be("ALICE_OUT");
        bob.CustomName.Should().Be(string.Empty);
    }

    [Fact]
    public async Task Apply_DisablesParticipantsThatPresetSaysOff_AndAreCurrentlyEnabled()
    {
        var controller = new StubIsoController();
        var alice = MakeParticipant(controller, "Alice", isEnabled: true);
        var preset = Preset(("Alice", false, null));

        var result = await PresetApplier.ApplyAsync(preset, new[] { alice }, controller, dispatcher: null);

        result.Matched.Should().Be(1);
        result.Changed.Should().Be(1);
        controller.DisableCalls.Should().ContainSingle().Which.Should().Be(alice.Id);
        alice.IsEnabled.Should().BeFalse();
    }

    [Fact]
    public async Task Apply_NoControllerCall_WhenStateAlreadyMatchesPreset()
    {
        var controller = new StubIsoController();
        var alice = MakeParticipant(controller, "Alice", isEnabled: true);
        var preset = Preset(("Alice", true, null));

        var result = await PresetApplier.ApplyAsync(preset, new[] { alice }, controller, dispatcher: null);

        result.Matched.Should().Be(1);
        result.Changed.Should().Be(0,
            "the participant is already enabled; preset says enabled â€” no controller traffic");
        controller.EnableCalls.Should().BeEmpty();
        controller.DisableCalls.Should().BeEmpty();
    }

    [Fact]
    public async Task Apply_MatchesByDisplayName_CaseInsensitive()
    {
        // Operator typed "Alice" when saving the preset; the live
        // participant comes back as "alice". The join must be case-
        // insensitive or the preset never finds the row.
        var controller = new StubIsoController();
        var alice = MakeParticipant(controller, "alice", isEnabled: false);
        var preset = Preset(("Alice", true, null));

        var result = await PresetApplier.ApplyAsync(preset, new[] { alice }, controller, dispatcher: null);

        result.Matched.Should().Be(1);
        alice.IsEnabled.Should().BeTrue();
    }

    [Fact]
    public async Task Apply_CountsSkipped_WhenPresetReferencesAbsentParticipants()
    {
        var controller = new StubIsoController();
        var alice = MakeParticipant(controller, "Alice", isEnabled: false);
        // Preset names Alice + a Bob who never joined.
        var preset = Preset(("Alice", true, null), ("Bob", true, null));

        var result = await PresetApplier.ApplyAsync(preset, new[] { alice }, controller, dispatcher: null);

        result.Matched.Should().Be(1);
        result.Skipped.Should().Be(1, "Bob is named in the preset but not in the meeting");
        result.Changed.Should().Be(1);
    }

    [Fact]
    public async Task Apply_IgnoresLiveParticipantsThatThePresetDoesntName()
    {
        // Carol joined the meeting but the saved preset only references
        // Alice. Carol's row must NOT be touched (no enable / disable
        // / customName change).
        var controller = new StubIsoController();
        var alice = MakeParticipant(controller, "Alice", isEnabled: false);
        var carol = MakeParticipant(controller, "Carol", isEnabled: true);
        var carolCustomBefore = carol.CustomName;
        var preset = Preset(("Alice", true, null));

        await PresetApplier.ApplyAsync(preset, new[] { alice, carol }, controller, dispatcher: null);

        carol.IsEnabled.Should().BeTrue("Carol wasn't named, so her state stands");
        carol.CustomName.Should().Be(carolCustomBefore);
        controller.EnableCalls.Should().ContainSingle().Which.Id.Should().Be(alice.Id);
        controller.DisableCalls.Should().BeEmpty();
    }
}
