using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Pcg;
using PkmnLib.Dynamic.Models;
using PkmnLib.Plugin.Gen7.Scripts.Pokemon;
using PkmnLib.Static;
using PkmnLib.Static.Species;
using PkmnLib.Static.Utils;
using PkmnLib.Tests.Integration;

namespace PkmnLib.Tests.Static;

public class DeepCloneTests
{
    [SuppressMessage("ReSharper", "UnusedMember.Local"), SuppressMessage("ReSharper", "ValueParameterNotUsed")]
    private class TestClass : IDeepCloneable
    {
        public int Value { get; set; }
        public int Field;
        private int PrivateValue { get; set; }
#pragma warning disable CS0169 // Field is never used
        private int _privateField;
#pragma warning restore CS0169 // Field is never used
        private int OnlyGetter => 0;

        private int OnlySetter
        {
            set { }
        }

        public TestClass? Self { get; set; }
    }

    [Test]
    public async Task DeepCloneTestProperty()
    {
        var obj = new TestClass { Value = 1 };
        var clone = obj.DeepClone();
        await Assert.That(clone).IsNotEqualTo(obj);
        await Assert.That(clone.Value).IsEqualTo(1);
    }

    [Test]
    public async Task DeepCloneTestField()
    {
        var obj = new TestClass { Field = 1 };
        var clone = obj.DeepClone();
        await Assert.That(clone).IsNotEqualTo(obj);
        await Assert.That(clone.Field).IsEqualTo(1);
    }

    [Test]
    public async Task DeepCloneTestPrivateProperty()
    {
        var obj = new TestClass();
        obj.GetType().GetProperty("PrivateValue", BindingFlags.NonPublic | BindingFlags.Instance)!.SetValue(obj, 1);
        var clone = obj.DeepClone();
        await Assert.That(clone).IsNotEqualTo(obj);
        var clonePrivateValue =
            clone.GetType().GetProperty("PrivateValue", BindingFlags.NonPublic | BindingFlags.Instance)!
                .GetValue(clone);
        await Assert.That((int)clonePrivateValue!).IsEqualTo(1);
    }

    [Test]
    public async Task DeepCloneTestPrivateField()
    {
        var obj = new TestClass();
        obj.GetType().GetField("_privateField", BindingFlags.NonPublic | BindingFlags.Instance)!.SetValue(obj, 1);
        var clone = obj.DeepClone();
        await Assert.That(clone).IsNotEqualTo(obj);
        var clonePrivateField =
            clone.GetType().GetField("_privateField", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(clone);
        await Assert.That((int)clonePrivateField!).IsEqualTo(1);
    }

    [Test]
    public async Task DeepCloneTestRecursion()
    {
        var obj = new TestClass();
        obj.Self = obj;
        var clone = obj.DeepClone();
        await Assert.That(clone).IsNotEqualTo(obj);
        await Assert.That(clone.Self).IsNotEqualTo(obj);
        await Assert.That(clone.Self).IsEqualTo(clone);
    }

    [Test]
    public async Task DeepCloneIntegrationTestsBattle()
    {
        var library = LibraryHelpers.LoadLibrary();
        await Assert.That(library.StaticLibrary.Species.TryGet("bulbasaur", out var bulbasaur)).IsTrue();
        await Assert.That(library.StaticLibrary.Species.TryGet("charmander", out var charmander)).IsTrue();
        var party1 = new PokemonPartyImpl(6);
        party1.SwapInto(new PokemonImpl(library, bulbasaur!, bulbasaur!.GetDefaultForm(), new AbilityIndex
        {
            IsHidden = false,
            Index = 0,
        }, 50, 0, Gender.Male, 0, "hardy"), 0);
        var party2 = new PokemonPartyImpl(6);
        party2.SwapInto(new PokemonImpl(library, charmander!, charmander!.GetDefaultForm(), new AbilityIndex
        {
            IsHidden = false,
            Index = 0,
        }, 50, 0, Gender.Male, 0, "hardy"), 0);

        var parties = new[]
        {
            new BattlePartyImpl(party1, [new ResponsibleIndex(0, 0)]),
            new BattlePartyImpl(party2, [new ResponsibleIndex(1, 0)]),
        };
        using var battle = new BattleImpl(library, parties, false, 2, 3, false, "grass", 0);
        battle.Sides[0].SwapPokemon(0, party1[0]);
        battle.Sides[1].SwapPokemon(0, party2[0]);
        party1[0]!.ChangeStatBoost(Statistic.Defense, 2, true, false);
        await Assert.That(party1[0]!.StatBoost.Defense).IsEqualTo((sbyte)2);
        party1[0]!.Volatile.Add(new ChargeBounceEffect(party1[0]!));

        var clone = battle.DeepClone();
        await Assert.That(clone).IsNotEqualTo(battle);
        await Assert.That(clone.Sides[0].Pokemon[0]).IsNotEqualTo(battle.Sides[0].Pokemon[0]!);
        await Assert.That(clone.Sides[1].Pokemon[0]).IsNotEqualTo(battle.Sides[1].Pokemon[0]!);

        await Assert.That(clone.Sides[0].Pokemon[0]!.Species).IsEqualTo(battle.Sides[0].Pokemon[0]!.Species);
        await Assert.That(clone.Sides[1].Pokemon[0]!.Species).IsEqualTo(battle.Sides[1].Pokemon[0]!.Species);

        await Assert.That(clone.Library).IsEqualTo(battle.Library);

        var pokemon = clone.Sides[0].Pokemon[0]!;
        await Assert.That(pokemon).IsNotNull();
        await Assert.That(pokemon).IsNotEqualTo(battle.Sides[0].Pokemon[0]!);
        await Assert.That(pokemon.BattleData).IsNotNull();
        await Assert.That(pokemon.BattleData).IsNotEqualTo(battle.Sides[0].Pokemon[0]!.BattleData!);
        await Assert.That(pokemon.BattleData!.Battle).IsEqualTo((IBattle)clone);
        await Assert.That(pokemon.BattleData!.SeenOpponents).Contains(clone.Sides[1].Pokemon[0]!);
        await Assert.That(pokemon.BattleData!.SeenOpponents).DoesNotContain(battle.Sides[1].Pokemon[0]!);
        await Assert.That(pokemon.StatBoost.Defense).IsEqualTo((sbyte)2);
        await Assert.That(pokemon.Volatile.Get<ChargeBounceEffect>()).IsNotNull();
        await Assert.That(pokemon.Volatile.Get<ChargeBounceEffect>()).IsNotEqualTo(
            battle.Sides[0].Pokemon[0]!.Volatile.Get<ChargeBounceEffect>()!);

        var ownerGetter =
            typeof(ChargeBounceEffect).GetField("_owner", BindingFlags.NonPublic | BindingFlags.Instance)!;
        var owner = ownerGetter.GetValue(pokemon.Volatile.Get<ChargeBounceEffect>()!);
        await Assert.That((IPokemon)owner!).IsEqualTo(pokemon);
        pokemon.Volatile.Remove<ChargeBounceEffect>();

        await Assert.That(pokemon.Volatile.Get<ChargeBounceEffect>()).IsNull();
        await Assert.That(battle.Sides[0].Pokemon[0]!.Volatile.Get<ChargeBounceEffect>()).IsNotNull();
    }

    /// <summary>
    /// We have custom handling for the random number generator within the deep cloning handling. We need to ensure that
    /// the random number generator is cloned correctly, so that the state of the random number generator is the same
    /// in the clone as it is in the original, while still being a different instance.
    /// </summary>
    [Test]
    public async Task DeepCloneIntegrationTestsBattleRandom()
    {
        var battleRandom = new BattleRandomImpl(0);
        battleRandom.GetInt();
        var clone = battleRandom.DeepClone();

        await Assert.That(clone).IsNotEqualTo(battleRandom);
        // We hack out way into the private fields of the random number generator to ensure that the state is the same
        // in the clone as it is in the original. None of this is part of the public API, so we use reflection to
        // access the private fields.
        var pcgRandomField = typeof(RandomImpl).GetField("_random", BindingFlags.NonPublic | BindingFlags.Instance)!;
        var pcgRandom = (PcgRandom)pcgRandomField.GetValue(battleRandom)!;
        var clonePcgRandom = (PcgRandom)pcgRandomField.GetValue(clone)!;
        await Assert.That(clonePcgRandom).IsNotEqualTo(pcgRandom);

        var pcgRngField = typeof(PcgRandom).GetField("_rng", BindingFlags.NonPublic | BindingFlags.Instance)!;
        var pcgRng = pcgRngField.GetValue(pcgRandom)!;
        var clonePcgRng = pcgRngField.GetValue(clonePcgRandom)!;

        var pcgStateField = pcgRng.GetType().GetField("_state", BindingFlags.NonPublic | BindingFlags.Instance)!;
        var pcgState = pcgStateField.GetValue(pcgRng);
        var clonePcgState = pcgStateField.GetValue(clonePcgRng);

        await Assert.That(clonePcgState).IsEqualTo(pcgState);

        var randomNumber = battleRandom.GetInt();
        var cloneRandomNumber = clone.GetInt();
        await Assert.That(cloneRandomNumber).IsEqualTo(randomNumber);
    }
}