using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;

namespace eagle0.Tests {
    [TestFixture]
    public class ProductionShaderReferenceTests {
        private static readonly string[] BuiltInOnlyPatterns = {
            "GrabPass",
            "#pragma surface",
            "UnityCG.cginc",
            "UnityUI.cginc",
            "UNITY_MATRIX_",
            "sampler2D _GrabTexture",
            "tessell",
            "Tessellation",
        };

        private static readonly string[] ShaderLikeExtensions = {
            ".shader",
            ".shadergraph",
        };

        private static readonly string[] ReferenceScanExtensions = {
            ".unity",
            ".prefab",
            ".asset",
            ".mat",
            ".shader",
            ".shadergraph",
        };

        private static readonly string[] ProductionReferenceRoots = {
            "Assets/Scenes/",
            "Assets/Eagle/",
            "Assets/Shardok/",
            "Assets/ConnectionHandler/",
            "Assets/UI/",
            "Assets/common/",
            "Assets/Tutorial/",
            "Assets/Resources/",
            "Assets/Materials/",
        };

        private static readonly string[] AllowedRiskyShaderRoots = {
            "Assets/TextMesh Pro/",
        };

        private static readonly Regex SerializedGuidRegex =
                new(@"\bguid:\s*([0-9a-f]{32})\b", RegexOptions.IgnoreCase);

        [Test]
        public void ProductionAssetsDoNotReferenceBuiltInOnlyShaders() {
            var projectRoot = Directory.GetParent(Application.dataPath).FullName;
            var riskyShaders = RiskyShaderGuids(projectRoot);
            var riskyMaterials = RiskyMaterialGuids(projectRoot, riskyShaders);
            var violations = new List<string>();

            foreach (var assetPath in ProductionReferencePaths(projectRoot)) {
                var text = File.ReadAllText(Path.Combine(projectRoot, assetPath));
                foreach (var guid in SerializedGuids(text)) {
                    if (riskyShaders.TryGetValue(guid, out var shader)) {
                        violations.Add($"{assetPath}: shader {shader.Path}");
                    }

                    if (riskyMaterials.TryGetValue(guid, out var material)) {
                        violations.Add(
                                $"{assetPath}: material {material.Path} uses shader {material.ShaderPath}");
                    }
                }
            }

            Assert.IsEmpty(
                    violations,
                    "Production serialized assets should not reference shaders with Built-in-only patterns, " +
                            "or materials that use them:\n" + string.Join("\n", violations));
        }

        [Test]
        public void ProductionMaterialsHaveAssignedShaders() {
            var violations = ProductionMaterialPaths()
                                     .Select(path => new {
                                         Path = path,
                                         Material = AssetDatabase.LoadAssetAtPath<Material>(path),
                                     })
                                     .Where(record => record.Material == null ||
                                                      record.Material.shader == null ||
                                                      record.Material.shader.name ==
                                                              "Hidden/InternalErrorShader")
                                     .Select(record => record.Path)
                                     .ToList();

            Assert.IsEmpty(
                    violations,
                    "Production materials must keep assigned shaders so URP/player builds do not render pink:\n" +
                            string.Join("\n", violations));
        }

        private static Dictionary<string, ShaderRecord> RiskyShaderGuids(string projectRoot) {
            return AssetFilePaths(projectRoot, new[] { "Assets/" }, ShaderLikeExtensions)
                    .Where(path => !IsAllowedRiskyShaderPath(path))
                    .Where(path => FileContainsAnyPattern(projectRoot, path, BuiltInOnlyPatterns))
                    .Select(path => new ShaderRecord {
                        Guid = AssetGuid(projectRoot, path),
                        Path = path,
                    })
                    .Where(record => !string.IsNullOrEmpty(record.Guid))
                    .ToDictionary(record => record.Guid, StringComparer.OrdinalIgnoreCase);
        }

        private static Dictionary<string, MaterialRecord> RiskyMaterialGuids(
                string projectRoot,
                IReadOnlyDictionary<string, ShaderRecord> riskyShaders) {
            var results = new Dictionary<string, MaterialRecord>(StringComparer.OrdinalIgnoreCase);
            foreach (var path in AssetFilePaths(
                             projectRoot,
                             new[] { "Assets/" },
                             new[] { ".mat" })) {
                var fullPath = Path.Combine(projectRoot, path);
                var text = File.ReadAllText(fullPath);
                foreach (var guid in SerializedGuids(text)) {
                    if (!riskyShaders.TryGetValue(guid, out var shader)) { continue; }

                    var materialGuid = AssetGuid(projectRoot, path);
                    if (string.IsNullOrEmpty(materialGuid)) { continue; }

                    results[materialGuid] = new MaterialRecord {
                        Guid = materialGuid,
                        Path = path,
                        ShaderPath = shader.Path,
                    };
                    break;
                }
            }

            return results;
        }

        private static IEnumerable<string> ProductionReferencePaths(string projectRoot) {
            return AssetFilePaths(projectRoot, ProductionReferenceRoots, ReferenceScanExtensions);
        }

        private static IEnumerable<string> ProductionMaterialPaths() {
            return AssetDatabase.FindAssets("t:Material")
                    .Select(AssetDatabase.GUIDToAssetPath)
                    .Where(IsProductionReferencePath)
                    .OrderBy(path => path, StringComparer.Ordinal);
        }

        private static bool IsAllowedRiskyShaderPath(string assetPath) {
            return AllowedRiskyShaderRoots.Any(
                    root => assetPath.StartsWith(root, StringComparison.Ordinal));
        }

        private static bool IsProductionReferencePath(string assetPath) {
            return ProductionReferenceRoots.Any(
                    root => assetPath.StartsWith(root, StringComparison.Ordinal));
        }

        private static IEnumerable<string> AssetFilePaths(
                string projectRoot,
                IEnumerable<string> assetRoots,
                IReadOnlyCollection<string> extensions) {
            return assetRoots.Select(root => Path.Combine(projectRoot, root))
                    .Where(Directory.Exists)
                    .SelectMany(
                            root => Directory
                                            .EnumerateFiles(root, "*", SearchOption.AllDirectories))
                    .Where(path => extensions.Contains(
                                   Path.GetExtension(path),
                                   StringComparer.OrdinalIgnoreCase))
                    .Select(path => RelativeAssetPath(projectRoot, path))
                    .OrderBy(path => path, StringComparer.Ordinal);
        }

        private static string AssetGuid(string projectRoot, string assetPath) {
            var metaPath = Path.Combine(projectRoot, assetPath + ".meta");
            if (!File.Exists(metaPath)) { return null; }

            var match = SerializedGuidRegex.Match(File.ReadAllText(metaPath));
            return match.Success ? match.Groups[1].Value : null;
        }

        private static string RelativeAssetPath(string projectRoot, string path) {
            return path.Substring(projectRoot.Length + 1).Replace(Path.DirectorySeparatorChar, '/');
        }

        private static bool
        FileContainsAnyPattern(string projectRoot, string assetPath, IEnumerable<string> patterns) {
            var fullPath = Path.Combine(projectRoot, assetPath);
            if (!File.Exists(fullPath)) { return false; }

            var text = File.ReadAllText(fullPath);
            return patterns.Any(
                    pattern => text.IndexOf(pattern, StringComparison.OrdinalIgnoreCase) >= 0);
        }

        private static IEnumerable<string> SerializedGuids(string text) {
            var seenGuids = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            foreach (Match match in SerializedGuidRegex.Matches(text)) {
                if (seenGuids.Add(match.Groups[1].Value)) { yield return match.Groups[1].Value; }
            }
        }

        private struct ShaderRecord {
            public string Guid;
            public string Path;
        }

        private struct MaterialRecord {
            public string Guid;
            public string Path;
            public string ShaderPath;
        }
    }
}
