using RioJoy.Core.Calibration;
using RioJoy.Core.Output;
using RioJoy.Core.Profiles;
using Xunit;

namespace RioJoy.Core.Tests.Profiles;

public class ConfigStoreTests
{
    [Fact]
    public void RoundTrips_FullConfig()
    {
        var config = new AppConfig
        {
            DefaultRioComPort = "COM3",
            DefaultPlasmaComPort = "COM4",
            NativeGameExecutables = { "firestorm.exe", "redplanet.exe" },
            NeutralProfileName = "Desktop",
            Profiles =
            {
                new RioProfile
                {
                    Name = "Doom",
                    RioComPort = "COM5",
                    PlasmaGreeting = "DOOM",
                    Buttons = { [0] = 0x8049, [0x50] = 0x1009 },
                    Calibration = new AxisCalibrationConfig { InvertY = true, EnableZR = false },
                    MatchExecutables = { "doom.exe" },
                },
            },
        };

        string json = ConfigStore.Serialize(config);
        AppConfig back = ConfigStore.Deserialize(json);

        Assert.Equal("COM3", back.DefaultRioComPort);
        Assert.Equal(new[] { "firestorm.exe", "redplanet.exe" }, back.NativeGameExecutables);

        RioProfile p = Assert.Single(back.Profiles);
        Assert.Equal("Doom", p.Name);
        Assert.Equal("COM5", p.RioComPort);
        Assert.Equal(0x8049, p.Buttons[0]);
        Assert.Equal(0x1009, p.Buttons[0x50]);
        Assert.True(p.Calibration.InvertY);
        Assert.False(p.Calibration.EnableZR);
        Assert.Equal(new[] { "doom.exe" }, p.MatchExecutables);
    }

    [Fact]
    public void RoundTrips_AxisRouting_WithStringEnums()
    {
        var config = new AppConfig
        {
            Profiles =
            {
                new RioProfile
                {
                    Name = "Descent",
                    AxisRouting = new AxisRoutingConfig
                    {
                        Z = new AxisRoute { Target = PadTarget.RightThumbY, Mode = AxisOutputMode.UnipolarPositive },
                        Rz = new AxisRoute { Target = PadTarget.RightThumbX },
                        Rx = new AxisRoute { Target = PadTarget.None },
                        Ry = new AxisRoute { Target = PadTarget.None },
                    },
                },
            },
        };

        string json = ConfigStore.Serialize(config);
        Assert.Contains("\"RightThumbY\"", json);       // enums serialize as strings
        Assert.Contains("\"UnipolarPositive\"", json);  // (house StringEnumConverter convention)

        RioProfile back = Assert.Single(ConfigStore.Deserialize(json).Profiles);
        Assert.Equal(config.Profiles[0].AxisRouting, back.AxisRouting); // record value equality
        Assert.Equal(PadTarget.LeftThumbX, back.AxisRouting!.X.Target); // untouched routes keep defaults
    }

    [Fact]
    public void AxisRouting_Unset_StaysNull_AndOffJson()
    {
        // null = default routing; NullValueHandling.Ignore keeps it out of the JSON,
        // so pre-existing profiles on disk are byte-compatible.
        string json = ConfigStore.Serialize(new AppConfig { Profiles = { new RioProfile { Name = "P" } } });
        Assert.DoesNotContain("AxisRouting", json);
        Assert.Null(Assert.Single(ConfigStore.Deserialize(json).Profiles).AxisRouting);
    }

    [Fact]
    public void ShippedDescentProfile_ParsesWithDescentRouting_NoTriggerTargets()
    {
        // Guards the shipped fixture itself: Newtonsoft's default
        // MissingMemberHandling.Ignore silently drops a misspelled property or axis
        // name, which would fall back to the default routing and put the throttle
        // back on LeftTrigger — the stock fire button in Descent's SDL
        // GameController mapping. So parse the real file, not a C#-built config.
        string json = File.ReadAllText(Path.Combine(TestRepo.Root(), "profiles", "descent-d1x.json"));

        // The shipped file is a single-profile document users paste into the
        // config's Profiles list; wrap it so it flows through the exact
        // ConfigStore serializer settings.
        RioProfile p = Assert.Single(ConfigStore.Deserialize($"{{\"Profiles\":[{json}]}}").Profiles);

        Assert.Equal("Descent", p.Name);
        Assert.True(p.Calibration.EnableZR); // pedal-differential rudder feeds Rz

        Assert.NotNull(p.AxisRouting);
        AxisRoutingConfig routing = p.AxisRouting!;
        Assert.Equal(new AxisRoute { Target = PadTarget.LeftThumbX }, routing.X);
        Assert.Equal(new AxisRoute { Target = PadTarget.LeftThumbY }, routing.Y);
        Assert.Equal(new AxisRoute { Target = PadTarget.RightThumbY, Mode = AxisOutputMode.UnipolarPositive }, routing.Z);
        Assert.Equal(new AxisRoute { Target = PadTarget.RightThumbX }, routing.Rz);
        Assert.Equal(new AxisRoute { Target = PadTarget.None }, routing.Rx);
        Assert.Equal(new AxisRoute { Target = PadTarget.None }, routing.Ry);

        // No axis may resolve onto a trigger — triggers are Descent's fire buttons.
        foreach (JoyAxis axis in new[] { JoyAxis.X, JoyAxis.Y, JoyAxis.Z, JoyAxis.Rx, JoyAxis.Ry, JoyAxis.Rz })
        {
            PadTarget target = AxisRouter.Resolve(routing, axis, AxisOutputs.Center).Target;
            Assert.NotEqual(PadTarget.LeftTrigger, target);
            Assert.NotEqual(PadTarget.RightTrigger, target);
        }
    }

    [Fact]
    public void ShippedDescentProfile_MatchesDxxRebirthReferenceCopy()
    {
        // The dxx-rebirth docs keep a reference copy of the Descent profile that
        // must stay byte-identical to ours. The sibling checkout is a cabinet-
        // machine convention, not a repo guarantee, so no-op quietly when the
        // second repo is not checked out next to this one.
        string reference = Path.GetFullPath(Path.Combine(
            TestRepo.Root(), "..", "dxx-rebirth", "docs", "reference", "descent-riojoy-profile.json"));
        if (!File.Exists(reference))
            return;

        string shipped = Path.Combine(TestRepo.Root(), "profiles", "descent-d1x.json");
        Assert.Equal(File.ReadAllBytes(shipped), File.ReadAllBytes(reference));
    }

    [Fact]
    public void ImportProfile_ShippedDescentFile_IntoFreshConfig()
    {
        string configPath = Path.Combine(Path.GetTempPath(), $"riojoy-import-{Guid.NewGuid():N}", "config.json");
        try
        {
            string shipped = Path.Combine(TestRepo.Root(), "profiles", "descent-d1x.json");
            ProfileImportResult result = ConfigStore.ImportProfile(configPath, shipped);

            Assert.Equal("Descent", result.Name);
            Assert.False(result.Replaced);

            RioProfile p = Assert.Single(ConfigStore.Load(configPath).Profiles);
            Assert.Equal("Descent", p.Name);
            // The routing must survive the import round-trip intact — this is the
            // exact payload a cabinet/bench install writes.
            Assert.NotNull(p.AxisRouting);
            Assert.Equal(new AxisRoute { Target = PadTarget.RightThumbY, Mode = AxisOutputMode.UnipolarPositive }, p.AxisRouting!.Z);
        }
        finally
        {
            string? dir = Path.GetDirectoryName(configPath);
            if (dir is not null && Directory.Exists(dir))
                Directory.Delete(dir, recursive: true);
        }
    }

    [Fact]
    public void ImportProfile_ReplacesSameName_PreservesEverythingElse()
    {
        string configPath = Path.Combine(Path.GetTempPath(), $"riojoy-import-{Guid.NewGuid():N}", "config.json");
        try
        {
            ConfigStore.Save(new AppConfig
            {
                DefaultRioComPort = "COM7",
                NeutralProfileName = "Desktop",
                Profiles =
                {
                    new RioProfile { Name = "Desktop" },
                    new RioProfile { Name = "descent", PlasmaGreeting = "STALE" }, // case-insensitive match
                },
            }, configPath);

            string shipped = Path.Combine(TestRepo.Root(), "profiles", "descent-d1x.json");
            ProfileImportResult result = ConfigStore.ImportProfile(configPath, shipped);
            Assert.True(result.Replaced);

            AppConfig back = ConfigStore.Load(configPath);
            Assert.Equal("COM7", back.DefaultRioComPort);       // untouched settings survive
            Assert.Equal("Desktop", back.NeutralProfileName);
            Assert.Equal(2, back.Profiles.Count);               // replaced in place, not appended
            RioProfile descent = Assert.IsType<RioProfile>(back.FindProfile("Descent"));
            Assert.Equal("DESCENT", descent.PlasmaGreeting);    // stale profile fully overwritten
        }
        finally
        {
            string? dir = Path.GetDirectoryName(configPath);
            if (dir is not null && Directory.Exists(dir))
                Directory.Delete(dir, recursive: true);
        }
    }

    [Fact]
    public void ImportProfile_NamelessProfile_Throws()
    {
        string dir = Path.Combine(Path.GetTempPath(), $"riojoy-import-{Guid.NewGuid():N}");
        Directory.CreateDirectory(dir);
        try
        {
            string profilePath = Path.Combine(dir, "nameless.json");
            File.WriteAllText(profilePath, "{\"PlasmaGreeting\":\"X\"}");
            Assert.Throws<Newtonsoft.Json.JsonSerializationException>(
                () => ConfigStore.ImportProfile(Path.Combine(dir, "config.json"), profilePath));
        }
        finally
        {
            Directory.Delete(dir, recursive: true);
        }
    }

    [Fact]
    public void Load_MissingFile_ReturnsDefaults()
    {
        string path = Path.Combine(Path.GetTempPath(), $"riojoy-missing-{Guid.NewGuid():N}.json");
        AppConfig config = ConfigStore.Load(path);
        Assert.Empty(config.Profiles);
        Assert.Equal("COM1", config.DefaultRioComPort);
    }

    [Fact]
    public void Save_ThenLoad_File()
    {
        string path = Path.Combine(Path.GetTempPath(), $"riojoy-{Guid.NewGuid():N}", "config.json");
        try
        {
            var config = new AppConfig { DefaultRioComPort = "COM9" };
            ConfigStore.Save(config, path);
            Assert.True(File.Exists(path));
            Assert.Equal("COM9", ConfigStore.Load(path).DefaultRioComPort);
        }
        finally
        {
            string? dir = Path.GetDirectoryName(path);
            if (dir is not null && Directory.Exists(dir))
                Directory.Delete(dir, recursive: true);
        }
    }
}
