using System;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;

namespace Mirror.Tests.ClientSceneTests
{
    public class PayloadTestBehaviour : NetworkBehaviour
    {
        public int value;
        public Vector3 direction;

        public event Action OnDeserializeCalled;
        public event Action OnSerializeCalled;

        public override bool OnSerialize(NetworkWriter writer, bool initialState)
        {
            base.OnSerialize(writer, initialState);

            writer.WriteInt(value);
            writer.WriteVector3(direction);

            OnSerializeCalled?.Invoke();

            return true;
        }
        public override void OnDeserialize(NetworkReader reader, bool initialState)
        {
            base.OnDeserialize(reader, initialState);

            value = reader.ReadInt();
            direction = reader.ReadVector3();

            OnDeserializeCalled?.Invoke();
        }
    }

    public class BehaviourWithEvents : NetworkBehaviour
    {
        public event Action OnStartAuthorityCalled;
        public event Action OnStartClientCalled;
        public event Action OnStartLocalPlayerCalled;

        public override void OnStartAuthority()
        {
            OnStartAuthorityCalled?.Invoke();
        }
        public override void OnStartClient()
        {
            OnStartClientCalled?.Invoke();
        }
        public override void OnStartLocalPlayer()
        {
            OnStartLocalPlayerCalled?.Invoke();
        }
    }

    public class ClientSceneTests_OnSpawn : ClientSceneTestsBase
    {
        Dictionary<uint, NetworkIdentity> spawned => NetworkClient.spawned;

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

        [Test]
        public void FindOrSpawnObject_FindExistingObject()
        {
            CreateNetworked(out _, out NetworkIdentity existing);
            const uint netId = 1000;
            existing.netId = netId;
            spawned.Add(netId, existing);

            SpawnMessage msg = new SpawnMessage
            {
                netId = netId
            };
            bool success = NetworkClient.FindOrSpawnObject(msg, out NetworkIdentity found);

            Assert.IsTrue(success);
            Assert.That(found, Is.EqualTo(existing));
        }

        [Test]
        public void FindOrSpawnObject_ErrorWhenNoExistingAndAssetIdAndSceneIdAreBothEmpty()
        {
            const uint netId = 1001;
            SpawnMessage msg = new SpawnMessage
            {
                assetId = new Guid(),
                sceneId = 0,
                netId = netId
            };

            LogAssert.Expect(LogType.Error, $"OnSpawn message with netId '{netId}' has no AssetId or sceneId");
            bool success = NetworkClient.FindOrSpawnObject(msg, out NetworkIdentity networkIdentity);

            Assert.IsFalse(success);
            Assert.IsNull(networkIdentity);
        }

        [Test]
        public void FindOrSpawnObject_SpawnsFromPrefabDictionary()
        {
            const uint netId = 1002;
            SpawnMessage msg = new SpawnMessage
            {
                netId = netId,
                assetId = validPrefabGuid

            };

            NetworkClient.prefabs.Add(validPrefabGuid, validPrefab);

            bool success = NetworkClient.FindOrSpawnObject(msg, out NetworkIdentity networkIdentity);

            Assert.IsTrue(success);
            Assert.IsNotNull(networkIdentity);
            Assert.That(networkIdentity.name, Is.EqualTo($"{validPrefab.name}(Clone)"));

            // cleanup
            GameObject.DestroyImmediate(networkIdentity.gameObject);
        }

        [Test]
        public void FindOrSpawnObject_ErrorWhenPrefabInNullInDictionary()
        {
            const uint netId = 1002;
            SpawnMessage msg = new SpawnMessage
            {
                netId = netId,
                assetId = validPrefabGuid
            };

            // could happen if the prefab is destroyed or unloaded
            NetworkClient.prefabs.Add(validPrefabGuid, null);

            LogAssert.Expect(LogType.Error, $"Failed to spawn server object, did you forget to add it to the NetworkManager? assetId={msg.assetId} netId={msg.netId}");
            LogAssert.Expect(LogType.Error, $"Could not spawn assetId={msg.assetId} scene={msg.sceneId:X} netId={msg.netId}");
            bool success = NetworkClient.FindOrSpawnObject(msg, out NetworkIdentity networkIdentity);


            Assert.IsFalse(success);
            Assert.IsNull(networkIdentity);
        }

        [Test]
        public void FindOrSpawnObject_SpawnsFromPrefabIfBothPrefabAndHandlerExists()
        {
            const uint netId = 1003;
            int handlerCalled = 0;
            SpawnMessage msg = new SpawnMessage
            {
                netId = netId,
                assetId = validPrefabGuid
            };

            NetworkClient.prefabs.Add(validPrefabGuid, validPrefab);
            NetworkClient.spawnHandlers.Add(validPrefabGuid, x =>
            {
                handlerCalled++;
                CreateNetworked(out GameObject go, out NetworkIdentity _);
                return go;
            });


            bool success = NetworkClient.FindOrSpawnObject(msg, out NetworkIdentity networkIdentity);

            Assert.IsTrue(success);
            Assert.IsNotNull(networkIdentity);
            Assert.That(networkIdentity.name, Is.EqualTo($"{validPrefab.name}(Clone)"));
            Assert.That(handlerCalled, Is.EqualTo(0), "Handler should not have been called");
        }

        [Test]
        public void FindOrSpawnObject_SpawnHandlerCalledFromDictionary()
        {
            const uint netId = 1003;
            int handlerCalled = 0;
            SpawnMessage msg = new SpawnMessage
            {
                netId = netId,
                assetId = validPrefabGuid
            };

            GameObject createdInhandler = null;

            NetworkClient.spawnHandlers.Add(validPrefabGuid, x =>
            {
                handlerCalled++;
                Assert.That(x, Is.EqualTo(msg));
                CreateNetworked(out createdInhandler, out NetworkIdentity _);
                return createdInhandler;
            });

            bool success = NetworkClient.FindOrSpawnObject(msg, out NetworkIdentity networkIdentity);

            Assert.IsTrue(success);
            Assert.IsNotNull(networkIdentity);
            Assert.That(handlerCalled, Is.EqualTo(1));
            Assert.That(networkIdentity.gameObject, Is.EqualTo(createdInhandler), "Object returned should be the same object created by the spawn handler");
        }

        [Test]
        public void FindOrSpawnObject_ErrorWhenSpawnHanlderReturnsNull()
        {
            const uint netId = 1003;
            SpawnMessage msg = new SpawnMessage
            {
                netId = netId,
                assetId = validPrefabGuid
            };

            NetworkClient.spawnHandlers.Add(validPrefabGuid, (x) => null);

            LogAssert.Expect(LogType.Error, $"Spawn Handler returned null, Handler assetId '{msg.assetId}'");
            LogAssert.Expect(LogType.Error, $"Could not spawn assetId={msg.assetId} scene={msg.sceneId:X} netId={msg.netId}");
            bool success = NetworkClient.FindOrSpawnObject(msg, out NetworkIdentity networkIdentity);

            Assert.IsFalse(success);
            Assert.IsNull(networkIdentity);
        }
        [Test]
        public void FindOrSpawnObject_ErrorWhenSpawnHanlderReturnsWithoutNetworkIdentity()
        {
            const uint netId = 1003;
            SpawnMessage msg = new SpawnMessage
            {
                netId = netId,
                assetId = validPrefabGuid
            };

            NetworkClient.spawnHandlers.Add(validPrefabGuid, (x) =>
            {
                CreateGameObject(out GameObject go);
                return go;
            });

            LogAssert.Expect(LogType.Error, $"Object Spawned by handler did not have a NetworkIdentity, Handler assetId '{validPrefabGuid}'");
            LogAssert.Expect(LogType.Error, $"Could not spawn assetId={msg.assetId} scene={msg.sceneId:X} netId={msg.netId}");
            bool success = NetworkClient.FindOrSpawnObject(msg, out NetworkIdentity networkIdentity);

            Assert.IsFalse(success);
            Assert.IsNull(networkIdentity);
        }

        NetworkIdentity CreateSceneObject(ulong sceneId)
        {
            CreateNetworked(out _, out NetworkIdentity identity);
            // set sceneId to zero as it is set in onvalidate (does not set id at runtime)
            identity.sceneId = sceneId;
            NetworkClient.spawnableObjects.Add(sceneId, identity);
            return identity;
        }

        [Test]
        public void FindOrSpawnObject_UsesSceneIdToSpawnFromSpawnableObjectsDictionary()
        {
            const uint netId = 1003;
            const int sceneId = 100020;
            SpawnMessage msg = new SpawnMessage
            {
                netId = netId,
                sceneId = sceneId
            };

            NetworkIdentity sceneObject = CreateSceneObject(sceneId);


            bool success = NetworkClient.FindOrSpawnObject(msg, out NetworkIdentity networkIdentity);

            Assert.IsTrue(success);
            Assert.IsNotNull(networkIdentity);
            Assert.That(networkIdentity, Is.EqualTo(sceneObject));
        }

        [Test]
        public void FindOrSpawnObject_SpawnsUsingSceneIdInsteadOfAssetId()
        {
            const uint netId = 1003;
            const int sceneId = 100020;
            SpawnMessage msg = new SpawnMessage
            {
                netId = netId,
                sceneId = sceneId,
                assetId = validPrefabGuid
            };

            NetworkClient.prefabs.Add(validPrefabGuid, validPrefab);
            NetworkIdentity sceneObject = CreateSceneObject(sceneId);

            bool success = NetworkClient.FindOrSpawnObject(msg, out NetworkIdentity networkIdentity);

            Assert.IsTrue(success);
            Assert.IsNotNull(networkIdentity);
            Assert.That(networkIdentity, Is.EqualTo(sceneObject));
        }

        [Test]
        public void FindOrSpawnObject_ErrorWhenSceneIdIsNotInSpawnableObjectsDictionary()
        {
            const uint netId = 1004;
            const int sceneId = 100021;
            SpawnMessage msg = new SpawnMessage
            {
                netId = netId,
                sceneId = sceneId,
            };

            LogAssert.Expect(LogType.Error, $"Spawn scene object not found for {msg.sceneId:X}. Make sure that client and server use exactly the same project. This only happens if the hierarchy gets out of sync.");
            LogAssert.Expect(LogType.Error, $"Could not spawn assetId={msg.assetId} scene={msg.sceneId:X} netId={msg.netId}");
            bool success = NetworkClient.FindOrSpawnObject(msg, out NetworkIdentity networkIdentity);

            Assert.IsFalse(success);
            Assert.IsNull(networkIdentity);
        }


        [Test]
        public void ApplyPayload_AppliesTransform()
        {
            const uint netId = 1000;

            CreateNetworked(out GameObject _, out NetworkIdentity identity);

            Vector3 position = new Vector3(10, 0, 20);
            Quaternion rotation = Quaternion.Euler(0, 45, 0);
            Vector3 scale = new Vector3(1.5f, 1.5f, 1.5f);
            SpawnMessage msg = new SpawnMessage
            {
                netId = netId,
                isLocalPlayer = false,
                isOwner = false,
                sceneId = 0,
                assetId = Guid.Empty,
                // use local values for VR support
                position = position,
                rotation = rotation,
                scale = scale,

                payload = default,
            };

            NetworkClient.ApplySpawnPayload(identity, msg);

            Assert.That(identity.transform.position, Is.EqualTo(position));
            // use angle because of floating point numbers
            // only need to check if rotations are approximately equal
            Assert.That(Quaternion.Angle(identity.transform.rotation, rotation), Is.LessThan(0.0001f));
            Assert.That(identity.transform.localScale, Is.EqualTo(scale));
        }

        [Test]
        public void ApplyPayload_AppliesLocalValuesToTransform()
        {
            const uint netId = 1000;
            CreateGameObject(out GameObject parent);
            parent.transform.position = new Vector3(100, 20, 0);
            parent.transform.rotation = Quaternion.LookRotation(Vector3.left);
            parent.transform.localScale = Vector3.one * 2;

            CreateNetworked(out GameObject go, out NetworkIdentity identity);
            go.transform.parent = parent.transform;

            Vector3 position = new Vector3(10, 0, 20);
            Quaternion rotation = Quaternion.Euler(0, 45, 0);
            Vector3 scale = new Vector3(1.5f, 1.5f, 1.5f);
            SpawnMessage msg = new SpawnMessage
            {
                netId = netId,
                isLocalPlayer = false,
                isOwner = false,
                sceneId = 0,
                assetId = Guid.Empty,
                // use local values for VR support
                position = position,
                rotation = rotation,
                scale = scale,

                payload = default,
            };

            NetworkClient.ApplySpawnPayload(identity, msg);

            Assert.That(identity.transform.localPosition, Is.EqualTo(position));
            // use angle because of floating point numbers
            // only need to check if rotations are approximately equal
            Assert.That(Quaternion.Angle(identity.transform.localRotation, rotation), Is.LessThan(0.0001f));
            Assert.That(identity.transform.localScale, Is.EqualTo(scale));
        }

        [Test]
        [TestCase(true)]
        [TestCase(false)]
        public void ApplyPayload_AppliesAuthority(bool isOwner)
        {
            const uint netId = 1000;

            CreateNetworked(out _, out NetworkIdentity identity);

            SpawnMessage msg = new SpawnMessage
            {
                netId = netId,
                isLocalPlayer = false,
                isOwner = isOwner,
                sceneId = 0,
                assetId = Guid.Empty,
                // use local values for VR support
                position = Vector3.zero,
                rotation = Quaternion.identity,
                scale = Vector3.one,

                payload = default
            };

            // set to opposite to make sure it is changed
            identity.hasAuthority = !isOwner;

            NetworkClient.ApplySpawnPayload(identity, msg);

            Assert.That(identity.hasAuthority, Is.EqualTo(isOwner));
        }

        [Test]
        [TestCase(true)]
        [TestCase(false)]
        public void ApplyPayload_EnablesObject(bool startActive)
        {
            const uint netId = 1000;

            CreateNetworked(out GameObject go, out NetworkIdentity identity);
            go.SetActive(startActive);

            SpawnMessage msg = new SpawnMessage
            {
                netId = netId,
                isLocalPlayer = false,
                isOwner = false,
                sceneId = 0,
                assetId = Guid.Empty,
                // use local values for VR support
                position = Vector3.zero,
                rotation = Quaternion.identity,
                scale = Vector3.one,

                payload = default,
            };

            NetworkClient.ApplySpawnPayload(identity, msg);

            Assert.IsTrue(identity.gameObject.activeSelf);
        }

        [Test]
        public void ApplyPayload_SetsAssetId()
        {
            const uint netId = 1000;

            CreateNetworked(out _, out NetworkIdentity identity);

            Guid guid = Guid.NewGuid();
            SpawnMessage msg = new SpawnMessage
            {
                netId = netId,
                isLocalPlayer = false,
                isOwner = false,
                sceneId = 0,
                assetId = guid,
                // use local values for VR support
                position = Vector3.zero,
                rotation = Quaternion.identity,
                scale = Vector3.one,

                payload = default
            };

            NetworkClient.ApplySpawnPayload(identity, msg);

            Assert.IsTrue(identity.gameObject.activeSelf);

            Assert.That(identity.assetId, Is.EqualTo(guid));
        }

        [Test]
        public void ApplyPayload_DoesNotSetAssetIdToEmpty()
        {
            const uint netId = 1000;

            CreateNetworked(out _, out NetworkIdentity identity);
            Guid guid = Guid.NewGuid();
            identity.assetId = guid;

            SpawnMessage msg = new SpawnMessage
            {
                netId = netId,
                isLocalPlayer = false,
                isOwner = false,
                sceneId = 0,
                assetId = Guid.Empty,
                // use local values for VR support
                position = Vector3.zero,
                rotation = Quaternion.identity,
                scale = Vector3.one,

                payload = default
            };

            NetworkClient.ApplySpawnPayload(identity, msg);

            Assert.That(identity.assetId, Is.EqualTo(guid), "AssetId should not have changed");
        }

        [Test]
        public void ApplyPayload_LocalPlayerAddsIdentityToConnection()
        {
            Debug.Assert(NetworkClient.localPlayer == null, "LocalPlayer should be null before this test");
            const uint netId = 1000;

            CreateNetworked(out _, out NetworkIdentity identity);

            SpawnMessage msg = new SpawnMessage
            {
                netId = netId,
                isLocalPlayer = true,
                isOwner = true,
                sceneId = 0,
                assetId = Guid.Empty,
                // use local values for VR support
                position = Vector3.zero,
                rotation = Quaternion.identity,
                scale = Vector3.one,

                payload = default,
            };

            NetworkClient.connection = new FakeNetworkConnection();
            NetworkClient.ready = true;
            NetworkClient.ApplySpawnPayload(identity, msg);

            Assert.That(NetworkClient.localPlayer, Is.EqualTo(identity));
            Assert.That(NetworkClient.connection.identity, Is.EqualTo(identity));
        }

        [Test]
        public void ApplyPayload_LocalPlayerWarningWhenNoReadyConnection()
        {
            Debug.Assert(NetworkClient.localPlayer == null, "LocalPlayer should be null before this test");
            const uint netId = 1000;

            CreateNetworked(out _, out NetworkIdentity identity);

            SpawnMessage msg = new SpawnMessage
            {
                netId = netId,
                isLocalPlayer = true,
                isOwner = true,
                sceneId = 0,
                assetId = Guid.Empty,
                // use local values for VR support
                position = Vector3.zero,
                rotation = Quaternion.identity,
                scale = Vector3.one,

                payload = default,
            };


            LogAssert.Expect(LogType.Warning, "No ready connection found for setting player controller during InternalAddPlayer");
            NetworkClient.ApplySpawnPayload(identity, msg);

            Assert.That(NetworkClient.localPlayer, Is.EqualTo(identity));
        }

        [Flags]
        public enum SpawnFinishedState
        {
            isSpawnFinished = 1,
            hasAuthority = 2,
            isLocalPlayer = 4
        }

        [Test]
        public void OnSpawn_GiveNoExtraErrorsWhenPrefabIsntSpawned()
        {
            const int netId = 20033;
            Debug.Assert(spawned.Count == 0, "There should be no spawned objects before test");
            SpawnMessage msg = new SpawnMessage
            {
                netId = netId,
            };

            // Check for log that FindOrSpawnObject gives, and make sure there are no other error logs
            LogAssert.Expect(LogType.Error, $"OnSpawn message with netId '{netId}' has no AssetId or sceneId");
            NetworkClient.OnSpawn(msg);

            Assert.That(spawned, Is.Empty);
        }
    }
}