using System.IO;
using FluentAssertions;
using DragonISO.App.Services;

namespace DragonISO.App.Tests.Services;

/// <summary>
/// Unit tests for <see cref="OperatorPresetStore"/>. Each test redirects
/// the store's file path to a per-test temp path via the internal
/// <c>PathOverride</c> hook so the operator's real
/// <c>%LOCALAPPDATA%\Dragon-ISO\presets.json</c> is never touched.
///
/// IDisposable on the test class cleans up the temp path after each
/// test. Shares <see cref="PresetStoreCollection"/> with any other
/// class that mutates <see cref="OperatorPresetStore.PathOverride"/> â€”
/// xUnit's parallel execution would otherwise let a sibling class's
/// ctor clobber our path mid-test.
/// </summary>
[Collection(PresetStoreCollection.Name)]
public sealed class OperatorPresetStoreTests : IDisposable
{
    private readonly string _tempPath;

    public OperatorPresetStoreTests()
    {
        // Path.GetTempFileName creates a 0-byte file we don't want; we want the
        // path to start non-existent. So generate a unique name in the temp dir
        // without creating it, and let OperatorPresetStore.Save() create as needed.
        _tempPath = Path.Combine(
            Path.GetTempPath(),
            $"Dragon-ISO-presets-test-{Guid.NewGuid():N}.json");
        OperatorPresetStore.PathOverride = _tempPath;
    }

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

    [Fact]
    public void LoadAll_NoFile_ReturnsEmpty()
    {
        OperatorPresetStore.LoadAll().Should().BeEmpty();
    }

    [Fact]
    public void Save_ThenLoadAll_RoundTrips()
    {
        var preset = MakePreset("Friday Show", ("Jane", true), ("Bob", false));
        OperatorPresetStore.Save(preset);

        var all = OperatorPresetStore.LoadAll();
        all.Should().HaveCount(1);
        var loaded = all[0];
        loaded.Name.Should().Be("Friday Show");
        loaded.Assignments.Should().HaveCount(2);
        loaded.Assignments.Should().ContainSingle(a => a.DisplayName == "Jane" && a.Enabled);
        loaded.Assignments.Should().ContainSingle(a => a.DisplayName == "Bob" && !a.Enabled);
    }

    [Fact]
    public void Save_SameNameTwice_OverwritesNotDuplicates()
    {
        OperatorPresetStore.Save(MakePreset("ShowA", ("Jane", true)));
        OperatorPresetStore.Save(MakePreset("ShowA", ("Bob", true)));

        var all = OperatorPresetStore.LoadAll();
        all.Should().HaveCount(1);
        all[0].Assignments.Should().HaveCount(1);
        all[0].Assignments[0].DisplayName.Should().Be("Bob");
    }

    [Fact]
    public void Save_NameMatchIsCaseInsensitive()
    {
        OperatorPresetStore.Save(MakePreset("Friday Show", ("Jane", true)));
        OperatorPresetStore.Save(MakePreset("friday SHOW", ("Bob", true)));

        OperatorPresetStore.LoadAll().Should().HaveCount(1, because: "preset names dedupe case-insensitively");
    }

    [Fact]
    public void Find_LocatesByCaseInsensitiveName()
    {
        OperatorPresetStore.Save(MakePreset("Friday Show", ("Jane", true)));

        OperatorPresetStore.Find("friday show").Should().NotBeNull();
        OperatorPresetStore.Find("FRIDAY SHOW").Should().NotBeNull();
        OperatorPresetStore.Find("Saturday Show").Should().BeNull();
    }

    [Fact]
    public void Delete_RemovesPreset()
    {
        OperatorPresetStore.Save(MakePreset("ShowA", ("Jane", true)));
        OperatorPresetStore.Save(MakePreset("ShowB", ("Bob", true)));

        OperatorPresetStore.Delete("ShowA");

        var all = OperatorPresetStore.LoadAll();
        all.Should().HaveCount(1);
        all[0].Name.Should().Be("ShowB");
    }

    [Fact]
    public void Delete_MissingName_IsNoOp()
    {
        OperatorPresetStore.Save(MakePreset("ShowA", ("Jane", true)));

        OperatorPresetStore.Delete("Nonexistent");

        OperatorPresetStore.LoadAll().Should().HaveCount(1, because: "deleting a missing name shouldn't touch existing presets");
    }

    [Fact]
    public void Delete_PreservesStartupPreference_WhenDifferentNameDeleted()
    {
        OperatorPresetStore.Save(MakePreset("ShowA", ("Jane", true)));
        OperatorPresetStore.Save(MakePreset("ShowB", ("Bob", true)));
        OperatorPresetStore.MarkApplied("ShowA");
        OperatorPresetStore.SetAutoApplyOnStartup(true);

        OperatorPresetStore.Delete("ShowB");

        var pref = OperatorPresetStore.GetStartupPreference();
        pref.LastAppliedName.Should().Be("ShowA");
        pref.AutoApplyOnStartup.Should().BeTrue();
    }

    [Fact]
    public void Delete_ClearsLastAppliedName_WhenAppliedPresetDeleted()
    {
        OperatorPresetStore.Save(MakePreset("ShowA", ("Jane", true)));
        OperatorPresetStore.MarkApplied("ShowA");

        OperatorPresetStore.Delete("ShowA");

        var pref = OperatorPresetStore.GetStartupPreference();
        pref.LastAppliedName.Should().BeNull(
            because: "deleting the last-applied preset should clear it so we don't try to re-apply a missing preset on next launch");
    }

    [Fact]
    public void GetStartupPreference_DefaultsToFalseAndNull_WhenNoFile()
    {
        var pref = OperatorPresetStore.GetStartupPreference();
        pref.LastAppliedName.Should().BeNull();
        pref.AutoApplyOnStartup.Should().BeFalse();
    }

    [Fact]
    public void MarkApplied_PersistsAcrossSaves()
    {
        OperatorPresetStore.Save(MakePreset("ShowA", ("Jane", true)));
        OperatorPresetStore.MarkApplied("ShowA");

        OperatorPresetStore.Save(MakePreset("ShowB", ("Bob", true)));

        OperatorPresetStore.GetStartupPreference().LastAppliedName.Should().Be("ShowA",
            because: "Saving a different preset must not clear the last-applied marker");
    }

    [Fact]
    public void SetAutoApplyOnStartup_PersistsAcrossSaves()
    {
        OperatorPresetStore.SetAutoApplyOnStartup(true);
        OperatorPresetStore.Save(MakePreset("ShowA", ("Jane", true)));

        OperatorPresetStore.GetStartupPreference().AutoApplyOnStartup.Should().BeTrue();
    }

    [Fact]
    public void ExportImportRoundTrip_AddsAllPresetsOnEmptyTarget()
    {
        OperatorPresetStore.Save(MakePreset("ShowA", ("Jane", true)));
        OperatorPresetStore.Save(MakePreset("ShowB", ("Bob", false)));

        var bundle = OperatorPresetStore.ExportAllAsJson();

        // Wipe and re-import.
        File.Delete(_tempPath);
        OperatorPresetStore.LoadAll().Should().BeEmpty();

        var result = OperatorPresetStore.ImportBundle(bundle, overwrite: false);
        result.Added.Should().Be(2);
        result.Overwritten.Should().Be(0);
        result.Skipped.Should().Be(0);
        result.Error.Should().BeNull();

        OperatorPresetStore.LoadAll().Should().HaveCount(2);
    }

    [Fact]
    public void Import_OverwriteFalse_SkipsCollisions()
    {
        OperatorPresetStore.Save(MakePreset("ShowA", ("Jane", true)));
        var bundle = OperatorPresetStore.ExportAllAsJson();
        // Modify in-memory: change Jane â†’ Bob, then import without overwrite.
        OperatorPresetStore.Save(MakePreset("ShowA", ("Bob", true)));

        var result = OperatorPresetStore.ImportBundle(bundle, overwrite: false);

        result.Added.Should().Be(0);
        result.Skipped.Should().Be(1, because: "ShowA already exists locally and overwrite is off");
        var afterImport = OperatorPresetStore.LoadAll().Single();
        afterImport.Assignments.Single().DisplayName.Should().Be("Bob",
            because: "skip means the local Bob version stays");
    }

    [Fact]
    public void Import_OverwriteTrue_ReplacesCollisions()
    {
        OperatorPresetStore.Save(MakePreset("ShowA", ("Jane", true)));
        var bundle = OperatorPresetStore.ExportAllAsJson();
        OperatorPresetStore.Save(MakePreset("ShowA", ("Bob", true)));

        var result = OperatorPresetStore.ImportBundle(bundle, overwrite: true);

        result.Overwritten.Should().Be(1);
        result.Skipped.Should().Be(0);
        OperatorPresetStore.LoadAll().Single().Assignments.Single().DisplayName.Should().Be("Jane",
            because: "overwrite means the bundle's Jane version wins");
    }

    [Fact]
    public void Import_MalformedJson_ReturnsErrorAndPreservesState()
    {
        OperatorPresetStore.Save(MakePreset("ShowA", ("Jane", true)));
        var before = OperatorPresetStore.LoadAll().Count;

        var result = OperatorPresetStore.ImportBundle("not actually json", overwrite: true);

        result.Error.Should().NotBeNull();
        OperatorPresetStore.LoadAll().Count.Should().Be(before, because: "a malformed import must not delete existing data");
    }

    [Fact]
    public void LoadAll_GarbageInFile_ReturnsEmpty()
    {
        // Pre-populate the file with garbage to simulate a corruption / partial-write scenario.
        Directory.CreateDirectory(Path.GetDirectoryName(_tempPath)!);
        File.WriteAllText(_tempPath, "not valid json {{{");

        OperatorPresetStore.LoadAll().Should().BeEmpty(
            because: "we degrade gracefully on corrupt files rather than crash the host");
    }

    private static OperatorPresetStore.Preset MakePreset(string name, params (string DisplayName, bool Enabled)[] assignments) =>
        new(
            Name: name,
            SavedAt: DateTimeOffset.UnixEpoch,
            Assignments: assignments
                .Select(a => new OperatorPresetStore.Assignment(a.DisplayName, CustomOutputName: null, a.Enabled))
                .ToArray());
}
