using System.Collections.Generic;
using System.IO;
using System.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;

namespace eagle0.Tests {
    [TestFixture]
    public class ProvinceBeastsControllerTests {
        private const string EffectsFolder = "Assets/Eagle/Effects";
        private const string BeastEffectsLabel = "beast-effects";
        private const string FallbackEffectName = "BeastsEffect";
        private const string AnimalEffectScriptPath = "Assets/Eagle/AnimalEffect.cs";
        private const string DragonEffectScriptPath = "Assets/Eagle/DragonEffect.cs";
        private const string MonsterEffectScriptPath = "Assets/Eagle/MonsterEffect.cs";

        [Test]
        public void EveryKnownBeastExceptUnknownUsesSpecificAnimatedEffect() {
            var effectPrefabs = LoadEffectPrefabs();
            var beastNames = LoadBeastNames();

            foreach (var beastName in beastNames) {
                var effectPrefab =
                        eagle.ProvinceBeastsController.GetEffectPrefab(beastName, effectPrefabs);

                Assert.NotNull(effectPrefab, $"Beast '{beastName}' must resolve to an effect.");
                if (beastName == "unknown") {
                    Assert.AreEqual(FallbackEffectName, effectPrefab.name);
                } else {
                    Assert.AreNotEqual(
                            FallbackEffectName,
                            effectPrefab.name,
                            $"Beast '{beastName}' should use a specific animated effect, not the generic fallback.");
                }
            }
        }

        [Test]
        public void SquatterUsesPeasantHumanEffect() {
            var effectPrefabs = LoadEffectPrefabs();

            var effectPrefab =
                    eagle.ProvinceBeastsController.GetEffectPrefab("squatter", effectPrefabs);

            Assert.NotNull(effectPrefab);
            Assert.AreEqual("PeasantEffect", effectPrefab.name);
        }

        [Test]
        public void EveryKnownBeastEffectIsAvailableThroughAddressables() {
            var effectPrefabs = LoadAddressableEffectPrefabs();
            var beastNames = LoadBeastNames();

            foreach (var beastName in beastNames) {
                var effectPrefab =
                        eagle.ProvinceBeastsController.GetEffectPrefab(beastName, effectPrefabs);

                Assert.NotNull(
                        effectPrefab,
                        $"Beast '{beastName}' must resolve through the '{BeastEffectsLabel}' Addressables label.");
                if (beastName == "unknown") {
                    Assert.AreEqual(FallbackEffectName, effectPrefab.name);
                } else {
                    Assert.AreNotEqual(
                            FallbackEffectName,
                            effectPrefab.name,
                            $"Beast '{beastName}' should have its specific effect prefab in the '{BeastEffectsLabel}' Addressables label.");
                }
            }
        }

        [Test]
        public void EveryKnownBeastEffectAddressIsAvailableThroughAddressables() {
            var entriesByAddress = LoadAddressableEffectEntriesByAddress();
            var beastNames = LoadBeastNames();

            foreach (var beastName in beastNames) {
                var effectAddress =
                        eagle.ProvinceBeastsController.GetEffectPrefabAddress(beastName);

                Assert.That(
                        entriesByAddress,
                        Contains.Key(effectAddress),
                        $"Beast '{beastName}' must resolve to an Addressables entry at '{effectAddress}'.");
            }
        }

        [Test]
        public void RendererBackedAddressableBeastEffectsUseCompatibilityControllers() {
            var violations = new List<string>();
            foreach (var effectPrefab in LoadAddressableEffectPrefabs().Values) {
                var rendererBackedPrefabs = RendererBackedPrefabNames(effectPrefab);
                if (rendererBackedPrefabs.Count == 0) { continue; }
                if (HasCompatibilityAwareController(effectPrefab)) { continue; }

                violations.Add(
                        $"{effectPrefab.name}: renderer-backed prefabs {string.Join(", ", rendererBackedPrefabs)}");
            }

            Assert.IsEmpty(
                    violations,
                    "Addressable beast effects that spawn renderer-backed prefabs must use a compatibility-aware controller " +
                            "so third-party materials are converted under URP:\n" +
                            string.Join("\n", violations));
        }

        [Test]
        public void CompatibilityAwareBeastControllersInvokeMaterialCompatibility() {
            AssertScriptInvokesMaterialCompatibility(AnimalEffectScriptPath);
            AssertScriptInvokesMaterialCompatibility(DragonEffectScriptPath);
            AssertScriptInvokesMaterialCompatibility(MonsterEffectScriptPath);
        }

        private static Dictionary<string, GameObject> LoadEffectPrefabs() {
            return AssetDatabase.FindAssets("t:Prefab", new[] { EffectsFolder })
                    .Select(AssetDatabase.GUIDToAssetPath)
                    .Select(AssetDatabase.LoadAssetAtPath<GameObject>)
                    .Where(prefab => prefab != null)
                    .ToDictionary(prefab => prefab.name);
        }

        private static Dictionary<string, AddressableAssetEntry>
        LoadAddressableEffectEntriesByAddress() {
            return LoadAddressableEffectEntries().ToDictionary(entry => entry.address);
        }

        private static Dictionary<string, GameObject> LoadAddressableEffectPrefabs() {
            return LoadAddressableEffectEntries()
                    .Select(LoadAddressablePrefab)
                    .Where(prefab => prefab != null)
                    .ToDictionary(prefab => prefab.name);
        }

        private static IEnumerable<AddressableAssetEntry> LoadAddressableEffectEntries() {
            var settings = AddressableAssetSettingsDefaultObject.Settings;
            Assert.NotNull(settings, "Addressable asset settings must exist.");
            Assert.Contains(
                    BeastEffectsLabel,
                    settings.GetLabels(),
                    $"Addressables must define the '{BeastEffectsLabel}' label.");

            return settings.groups.Where(group => group != null)
                    .SelectMany(group => group.entries)
                    .Where(entry => entry.labels.Contains(BeastEffectsLabel));
        }

        private static GameObject LoadAddressablePrefab(AddressableAssetEntry entry) {
            var assetPath = AssetDatabase.GUIDToAssetPath(entry.guid);
            Assert.IsNotEmpty(
                    assetPath,
                    $"Addressable entry '{entry.address}' must resolve to an asset path.");

            var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
            Assert.NotNull(
                    prefab,
                    $"Addressable entry '{entry.address}' must point to a GameObject prefab.");
            return prefab;
        }

        private static List<string> RendererBackedPrefabNames(GameObject effectPrefab) {
            var results = new List<string>();
            if (effectPrefab.GetComponentsInChildren<Renderer>(includeInactive: true).Any()) {
                results.Add(effectPrefab.name);
            }

            var animalEffect = effectPrefab.GetComponent<eagle.AnimalEffect>();
            if (animalEffect != null) {
                AddRendererBackedPrefabNames(
                        results,
                        effectPrefab.name,
                        "animalPrefabs",
                        animalEffect.animalPrefabs);
            }

            var dragonEffect = effectPrefab.GetComponent<eagle.DragonEffect>();
            if (dragonEffect != null) {
                AddRendererBackedPrefabNames(
                        results,
                        effectPrefab.name,
                        "dragonPrefab",
                        new[] { dragonEffect.dragonPrefab });
            }

            var monsterEffect = effectPrefab.GetComponent<eagle.MonsterEffect>();
            if (monsterEffect != null) {
                AddRendererBackedPrefabNames(
                        results,
                        effectPrefab.name,
                        "monsterPrefabs",
                        monsterEffect.monsterPrefabs);
            }

            return results;
        }

        private static void AddRendererBackedPrefabNames(
                List<string> results,
                string effectPrefabName,
                string fieldName,
                IEnumerable<GameObject> prefabs) {
            Assert.NotNull(
                    prefabs,
                    $"{effectPrefabName}.{fieldName} must be assigned for renderer-backed beast effects.");

            foreach (var prefab in prefabs) {
                Assert.NotNull(
                        prefab,
                        $"{effectPrefabName}.{fieldName} must not contain missing prefab references.");
                if (prefab.GetComponentsInChildren<Renderer>(includeInactive: true).Any()) {
                    results.Add(prefab.name);
                }
            }
        }

        private static bool HasCompatibilityAwareController(GameObject effectPrefab) {
            return effectPrefab.GetComponent<eagle.AnimalEffect>() != null ||
                   effectPrefab.GetComponent<eagle.DragonEffect>() != null ||
                   effectPrefab.GetComponent<eagle.MonsterEffect>() != null;
        }

        private static void AssertScriptInvokesMaterialCompatibility(string scriptPath) {
            StringAssert.Contains(
                    "BeastMaterialCompatibility.ConfigureRenderer",
                    File.ReadAllText(scriptPath),
                    $"{scriptPath} must keep routing spawned renderers through the URP compatibility material path.");
        }

        private static IEnumerable<string> LoadBeastNames() {
            var beastTablePath = FindResourcePath("net/eagle0/eagle/beasts.tsv");
            return File.ReadLines(beastTablePath)
                    .Skip(1)
                    .Where(line => !string.IsNullOrWhiteSpace(line))
                    .Select(line => line.Split('\t')[0]);
        }

        private static string FindResourcePath(string relativeResourcePath) {
            var directory = new DirectoryInfo(Application.dataPath);
            while (directory != null) {
                var candidate = Path.Combine(directory.FullName, "resources", relativeResourcePath);
                if (File.Exists(candidate)) { return candidate; }

                directory = directory.Parent;
            }

            Assert.Fail($"Could not find resource file '{relativeResourcePath}'.");
            return null;
        }
    }
}
