using NUnit.Framework;
using UnityEngine;

namespace Mirror.Tests.SyncVarAttributeTests
{
    class MockPlayer : NetworkBehaviour
    {
        public struct Guild
        {
            public string name;
        }

        [SyncVar]
        public Guild guild;
    }

    class SyncVarGameObject : NetworkBehaviour
    {
        [SyncVar]
        public GameObject value;
    }
    class SyncVarNetworkIdentity : NetworkBehaviour
    {
        [SyncVar]
        public NetworkIdentity value;
    }
    class SyncVarTransform : NetworkBehaviour
    {
        [SyncVar]
        public Transform value;
    }
    class SyncVarNetworkBehaviour : NetworkBehaviour
    {
        [SyncVar]
        public SyncVarNetworkBehaviour value;
    }
    class SyncVarAbstractNetworkBehaviour : NetworkBehaviour
    {
        public abstract class MockMonsterBase : NetworkBehaviour
        {
            public abstract string GetName();
        }

        public class MockZombie : MockMonsterBase
        {
            public override string GetName() => "Zombie";
        }

        public class MockWolf : MockMonsterBase
        {
            public override string GetName() => "Wolf";
        }

        [SyncVar]
        public MockMonsterBase monster1;

        [SyncVar]
        public MockMonsterBase monster2;
    }

    public class SyncVarAttributeTest : MirrorTest
    {
        [SetUp]
        public override void SetUp()
        {
            base.SetUp();

            // start server & connect client because we need spawn functions
            NetworkServer.Listen(1);

            // we are testing server->client syncs.
            // so we need truly separted server & client, not host.
            ConnectClientBlockingAuthenticatedAndReady(out _);
        }

        [TearDown]
        public override void TearDown()
        {
            base.TearDown();
        }

        [Test]
        public void TestSettingStruct()
        {
            CreateNetworked(out _, out _, out MockPlayer player);

            // synchronize immediately
            player.syncInterval = 0f;

            Assert.That(player.IsDirty(), Is.False, "First time object should not be dirty");

            MockPlayer.Guild myGuild = new MockPlayer.Guild
            {
                name = "Back street boys"
            };

            player.guild = myGuild;

            Assert.That(player.IsDirty(), "Setting struct should mark object as dirty");
            player.ClearAllDirtyBits();
            Assert.That(player.IsDirty(), Is.False, "ClearAllDirtyBits() should clear dirty flag");

            // clearing the guild should set dirty bit too
            player.guild = default;
            Assert.That(player.IsDirty(), "Clearing struct should mark object as dirty");
        }

        [Test]
        public void TestSyncIntervalAndClearDirtyComponents()
        {
            CreateNetworked(out _, out _, out MockPlayer player);
            player.lastSyncTime = NetworkTime.localTime;
            // synchronize immediately
            player.syncInterval = 1f;

            player.guild = new MockPlayer.Guild
            {
                name = "Back street boys"
            };

            Assert.That(player.IsDirty(), Is.False, "Sync interval not met, so not dirty yet");

            // ClearDirtyComponents should do nothing since syncInterval is not
            // elapsed yet
            player.netIdentity.ClearDirtyComponentsDirtyBits();

            // set lastSyncTime far enough back to be ready for syncing
            player.lastSyncTime = NetworkTime.localTime - player.syncInterval;

            // should be dirty now
            Assert.That(player.IsDirty(), Is.True, "Sync interval met, should be dirty");
        }

        [Test]
        public void TestSyncIntervalAndClearAllComponents()
        {
            CreateNetworked(out _, out _, out MockPlayer player);
            player.lastSyncTime = NetworkTime.localTime;
            // synchronize immediately
            player.syncInterval = 1f;

            player.guild = new MockPlayer.Guild
            {
                name = "Back street boys"
            };

            Assert.That(player.IsDirty(), Is.False, "Sync interval not met, so not dirty yet");

            // ClearAllComponents should clear dirty even if syncInterval not
            // elapsed yet
            player.netIdentity.ClearAllComponentsDirtyBits();

            // set lastSyncTime far enough back to be ready for syncing
            player.lastSyncTime = NetworkTime.localTime - player.syncInterval;

            // should be dirty now
            Assert.That(player.IsDirty(), Is.False, "Sync interval met, should still not be dirty");
        }

        [Test]
        public void SyncsGameobject()
        {
            CreateNetworkedAndSpawn(
                out _, out _, out SyncVarGameObject serverObject,
                out _, out _, out SyncVarGameObject clientObject);

            // create spawned because we will look up netId in .spawned
            CreateNetworkedAndSpawn(
                out GameObject serverValue, out _,
                out GameObject clientValue, out _);

            serverObject.value = serverValue;
            clientObject.value = null;

            ProcessMessages();
            Assert.That(clientObject.value, Is.EqualTo(clientValue));
        }

        [Test]
        public void SyncIdentity()
        {
            CreateNetworkedAndSpawn(
                out _, out _, out SyncVarNetworkIdentity serverObject,
                out _, out _, out SyncVarNetworkIdentity clientObject);

            // create spawned because we will look up netId in .spawned
            CreateNetworkedAndSpawn(
                out _, out NetworkIdentity serverValue,
                out _, out NetworkIdentity clientValue);

            serverObject.value = serverValue;
            clientObject.value = null;

            ProcessMessages();
            Assert.That(clientObject.value, Is.EqualTo(clientValue));
        }

        [Test]
        public void SyncTransform()
        {
            CreateNetworkedAndSpawn(
                out _, out _, out SyncVarTransform serverObject,
                out _, out _, out SyncVarTransform clientObject);

            // create spawned because we will look up netId in .spawned
            CreateNetworkedAndSpawn(
                out _, out NetworkIdentity serverIdentity,
                out _, out NetworkIdentity clientIdentity);

            Transform serverValue = serverIdentity.transform;
            Transform clientValue = clientIdentity.transform;

            serverObject.value = serverValue;
            clientObject.value = null;

            ProcessMessages();
            Assert.That(clientObject.value, Is.EqualTo(clientValue));
        }

        [Test]
        public void SyncsBehaviour()
        {
            CreateNetworkedAndSpawn(
                out _, out _, out SyncVarNetworkBehaviour serverObject,
                out _, out _, out SyncVarNetworkBehaviour clientObject);

            // create spawned because we will look up netId in .spawned
            CreateNetworkedAndSpawn(
                out _, out _, out SyncVarNetworkBehaviour serverValue,
                out _, out _, out SyncVarNetworkBehaviour clientValue);

            serverObject.value = serverValue;
            clientObject.value = null;

            ProcessMessages();
            Assert.That(clientObject.value, Is.EqualTo(clientValue));
        }

        [Test]
        public void SyncsMultipleBehaviour()
        {
            CreateNetworkedAndSpawn(
                out _, out _, out SyncVarNetworkBehaviour serverObject,
                out _, out _, out SyncVarNetworkBehaviour clientObject);

            // create spawned because we will look up netId in .spawned
            CreateNetworkedAndSpawn(
                out _, out NetworkIdentity serverIdentity, out SyncVarNetworkBehaviour serverBehaviour1, out SyncVarNetworkBehaviour serverBehaviour2,
                out _, out NetworkIdentity clientIdentity, out SyncVarNetworkBehaviour clientBehaviour1, out SyncVarNetworkBehaviour clientBehaviour2);

            // create array/set indices
            _ = serverIdentity.NetworkBehaviours;

            int index1 = serverBehaviour1.ComponentIndex;
            int index2 = serverBehaviour2.ComponentIndex;

            // check components of same type have different indexes
            Assert.That(index1, Is.Not.EqualTo(index2));

            // check behaviour 1 can be synced
            serverObject.value = serverBehaviour1;
            clientObject.value = null;

            ProcessMessages();
            Assert.That(clientObject.value, Is.EqualTo(clientBehaviour1));

            // check that behaviour 2 can be synced
            serverObject.value = serverBehaviour2;
            clientObject.value = null;

            ProcessMessages();
            Assert.That(clientObject.value, Is.EqualTo(clientBehaviour2));
        }

        // this test is also important if we do LocalWorldState later:
        // - if LocalWorldMessage spawns netId=N
        // - and we remove N from NetworkClient.spawned
        // - and the next LocalWorldMessage contains updated payload for N
        // =>  client should NOT assume it's a spawned payload just because the
        //     netId isn't in spawned anymore.
        [Test]
        public void SyncVarCacheNetidForGameObject()
        {
            CreateNetworkedAndSpawn(
                out _, out _, out SyncVarGameObject serverObject,
                out _, out _, out SyncVarGameObject clientObject);

            // create spawned because we will look up netId in .spawned
            CreateNetworkedAndSpawn(
                out GameObject serverValue, out NetworkIdentity serverIdentity,
                out GameObject clientValue, out NetworkIdentity clientIdentity);

            Assert.That(serverValue, Is.Not.Null, "getCreatedValue should not return null");

            serverObject.value = serverValue;
            clientObject.value = null;

            // remove identity from client, as if it walked out of range
            NetworkClient.spawned.Remove(clientIdentity.netId);

            ProcessMessages();

            // check field shows as null
            Assert.That(clientObject.value, Is.EqualTo(null), "field should return null");

            // add identity back to collection, as if it walked back into range
            NetworkClient.spawned.Add(clientIdentity.netId, clientIdentity);

            // check field finds value
            Assert.That(clientObject.value, Is.EqualTo(clientValue), "fields should return clientValue");
        }

        // this test is also important if we do LocalWorldState later:
        // - if LocalWorldMessage spawns netId=N
        // - and we remove N from NetworkClient.spawned
        // - and the next LocalWorldMessage contains updated payload for N
        // =>  client should NOT assume it's a spawned payload just because the
        //     netId isn't in spawned anymore.
        [Test]
        public void SyncVarCacheNetidForIdentity()
        {
            CreateNetworkedAndSpawn(
                out _, out _, out SyncVarNetworkIdentity serverObject,
                out _, out _, out SyncVarNetworkIdentity clientObject);

            // create spawned because we will look up netId in .spawned
            CreateNetworkedAndSpawn(
                out _, out NetworkIdentity serverValue,
                out _, out NetworkIdentity clientValue);

            Assert.That(serverValue, Is.Not.Null, "getCreatedValue should not return null");

            serverObject.value = serverValue;
            clientObject.value = null;

            // remove identity from client, as if it walked out of range
            NetworkClient.spawned.Remove(clientValue.netId);

            ProcessMessages();

            // check field shows as null
            Assert.That(clientObject.value, Is.EqualTo(null), "field should return null");

            // add identity back to collection, as if it walked back into range
            NetworkClient.spawned.Add(clientValue.netId, clientValue);

            // check field finds value
            Assert.That(clientObject.value, Is.EqualTo(clientValue), "fields should return clientValue");
        }

        // this test is also important if we do LocalWorldState later:
        // - if LocalWorldMessage spawns netId=N
        // - and we remove N from NetworkClient.spawned
        // - and the next LocalWorldMessage contains updated payload for N
        // =>  client should NOT assume it's a spawned payload just because the
        //     netId isn't in spawned anymore.
        [Test]
        public void SyncVarCacheNetidForBehaviour()
        {
            CreateNetworkedAndSpawn(
                out _, out _, out SyncVarNetworkBehaviour serverObject,
                out _, out _, out SyncVarNetworkBehaviour clientObject);

            // create spawned because we will look up netId in .spawned
            CreateNetworkedAndSpawn(
                out _, out NetworkIdentity serverIdentity, out SyncVarNetworkBehaviour serverValue,
                out _, out NetworkIdentity clientIdentity, out SyncVarNetworkBehaviour clientValue);

            Assert.That(serverValue, Is.Not.Null, "getCreatedValue should not return null");

            // set on server
            serverObject.value = serverValue;
            clientObject.value = null;

            // remove identity from client, as if it walked out of range
            NetworkClient.spawned.Remove(clientIdentity.netId);

            ProcessMessages();

            // check field shows as null
            Assert.That(clientObject.value, Is.EqualTo(null), "field should return null");

            // add identity back to collection, as if it walked back into range
            NetworkClient.spawned.Add(clientIdentity.netId, clientIdentity);

            // check field finds value
            Assert.That(clientObject.value, Is.EqualTo(clientValue), "fields should return clientValue");
        }

        [Test]
        public void TestSyncingAbstractNetworkBehaviour()
        {
            // set up a "server" object
            CreateNetworked(out _, out NetworkIdentity serverIdentity, out SyncVarAbstractNetworkBehaviour serverBehaviour);

            // spawn syncvar targets
            CreateNetworked(out _, out NetworkIdentity wolfIdentity, out SyncVarAbstractNetworkBehaviour.MockWolf wolf);
            CreateNetworked(out _, out NetworkIdentity zombieIdentity, out SyncVarAbstractNetworkBehaviour.MockZombie zombie);

            wolfIdentity.netId = 135;
            zombieIdentity.netId = 246;

            serverBehaviour.monster1 = wolf;
            serverBehaviour.monster2 = zombie;

            // serialize all the data as we would for the network
            NetworkWriter ownerWriter = new NetworkWriter();
            // not really used in this Test
            NetworkWriter observersWriter = new NetworkWriter();
            serverIdentity.OnSerializeAllSafely(true, ownerWriter, observersWriter);

            // set up a "client" object
            CreateNetworked(out _, out NetworkIdentity clientIdentity, out SyncVarAbstractNetworkBehaviour clientBehaviour);

            // apply all the data from the server object
            NetworkReader reader = new NetworkReader(ownerWriter.ToArray());
            clientIdentity.OnDeserializeAllSafely(reader, true);

            // check that the syncvars got updated
            Assert.That(clientBehaviour.monster1, Is.EqualTo(serverBehaviour.monster1), "Data should be synchronized");
            Assert.That(clientBehaviour.monster2, Is.EqualTo(serverBehaviour.monster2), "Data should be synchronized");
        }
    }
}