// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE

namespace WallstopStudios.UnityHelpers.Tests.Utils
{
#if UNITY_EDITOR
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.IO;
    using NUnit.Framework;
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.TestTools;
    using WallstopStudios.UnityHelpers.Core.Attributes;
    using WallstopStudios.UnityHelpers.Core.Helper;
    using WallstopStudios.UnityHelpers.Editor;
    using WallstopStudios.UnityHelpers.Editor.AssetProcessors;
    using WallstopStudios.UnityHelpers.Tags;
    using WallstopStudios.UnityHelpers.Utils;
    using WallstopStudios.UnityHelpers.Editor.Utils;
    using WallstopStudios.UnityHelpers.Tests.AssetProcessors;
    using WallstopStudios.UnityHelpers.Tests.Core;
    using Object = UnityEngine.Object;

    [TestFixture]
    [NUnit.Framework.Category("Slow")]
    [NUnit.Framework.Category("Integration")]
    public sealed class SingletonAssetPollutionPreventionTests : CommonTestBase
    {
        private const string ResourcesRoot = "Assets/Resources";

        private EditorAssetFileScope _productionAssets;
        private bool _previousIncludeTestAssemblies;
        private Func<Type, bool> _previousTypeFilter;
        private bool _previousEditorUiSuppress;
        private readonly List<string> _createdAssets = new();

        /// <summary>
        /// Data source for duplicate folder detection tests.
        /// Each entry contains: parentPath, folderBaseName
        /// </summary>
        private static IEnumerable<DuplicateFolderTestCase> DuplicateFolderTestCases()
        {
            yield return new DuplicateFolderTestCase(
                "Assets/Resources",
                "Wallstop Studios",
                "Duplicate Wallstop Studios folders may be created during test runs"
            );

            yield return new DuplicateFolderTestCase(
                "Assets/Resources/Wallstop Studios",
                "Unity Helpers",
                "Duplicate Unity Helpers folders may be created during concurrent test execution"
            );
        }

        private static string GetExpectedAssetPath(Type type)
        {
            if (
                ReflectionHelpers.TryGetAttributeSafe<ScriptableSingletonPathAttribute>(
                    type,
                    out ScriptableSingletonPathAttribute pathAttr,
                    inherit: false
                ) && !string.IsNullOrWhiteSpace(pathAttr.resourcesPath)
            )
            {
                return $"{ResourcesRoot}/{pathAttr.resourcesPath}/{type.Name}.asset";
            }

            return $"{ResourcesRoot}/{type.Name}.asset";
        }

        private static void DeleteAssetIfExists(string assetPath)
        {
            if (string.IsNullOrWhiteSpace(assetPath))
            {
                return;
            }

            Object existing = AssetDatabase.LoadAssetAtPath<Object>(assetPath);
            if (existing != null || !string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(assetPath)))
            {
                AssetDatabase.DeleteAsset(assetPath);
            }
        }

        private static void EnsureMetadataFolder()
        {
            string metadataFolder = "Assets/Resources/Wallstop Studios/Unity Helpers";
            // Create the directory on disk first, or Unity raises its "Moving file failed" modal.
            string projectRoot = Path.GetDirectoryName(Application.dataPath);
            if (!string.IsNullOrEmpty(projectRoot))
            {
                string absoluteDirectory = Path.Combine(projectRoot, metadataFolder);
                if (!Directory.Exists(absoluteDirectory))
                {
                    Directory.CreateDirectory(absoluteDirectory);
                }
            }

            string[] parts = metadataFolder.Split('/');
            string current = parts[0];
            for (int i = 1; i < parts.Length; ++i)
            {
                string next = current + "/" + parts[i];
                if (!AssetDatabase.IsValidFolder(next))
                {
                    AssetDatabase.CreateFolder(current, parts[i]);
                }
                current = next;
            }
        }

        private static void TryDeleteEmptyFolder(string folderPath)
        {
            if (string.IsNullOrWhiteSpace(folderPath))
            {
                return;
            }

            if (!AssetDatabase.IsValidFolder(folderPath))
            {
                return;
            }

            string[] subFolders = AssetDatabase.GetSubFolders(folderPath);
            if (subFolders != null && 0 < subFolders.Length)
            {
                return;
            }

            string[] assets = AssetDatabase.FindAssets(string.Empty, new[] { folderPath });
            if (assets != null && 0 < assets.Length)
            {
                return;
            }

            AssetDatabase.DeleteAsset(folderPath);
        }

        [OneTimeSetUp]
        public void OneTimeSetUp()
        {
            _productionAssets = new EditorAssetFileScope(
                PersistentDirectorySettings.TargetAssetPath,
                "Assets/Resources/Wallstop Studios/Unity Helpers/"
                    + nameof(AttributeMetadataCache)
                    + ".asset"
            );
            CleanupAllKnownTestFolders();
        }

        [UnitySetUp]
        public IEnumerator SetUp()
        {
            AssetPostprocessorTestHandlers.AssertCleanAndClearAll();

            _previousEditorUiSuppress = EditorUi.Suppress;
            EditorUi.Suppress = true;
            _previousIncludeTestAssemblies =
                ScriptableObjectSingletonCreatorTestAccess.IncludeTestAssemblies;
            _previousTypeFilter = ScriptableObjectSingletonCreatorTestAccess.TypeFilter;
            ScriptableObjectSingletonCreatorTestAccess.IncludeTestAssemblies = false;
            ScriptableObjectSingletonCreatorTestAccess.TypeFilter = null;
            ScriptableObjectSingletonCreatorTestAccess.DisableAutomaticRetries = true;
            yield return CleanupExistingTestSingletonAssets();
        }

        [UnityTearDown]
        public override IEnumerator UnityTearDown()
        {
            yield return base.UnityTearDown();
            foreach (string path in _createdAssets)
            {
                DeleteAssetIfExists(path);
                yield return null;
            }
            _createdAssets.Clear();
            yield return CleanupExistingTestSingletonAssets();
            yield return CleanupTestFolders();
            ScriptableObjectSingletonCreatorTestAccess.IncludeTestAssemblies =
                _previousIncludeTestAssemblies;
            ScriptableObjectSingletonCreatorTestAccess.TypeFilter = _previousTypeFilter;
            ScriptableObjectSingletonCreatorTestAccess.DisableAutomaticRetries = false;
            ScriptableObjectSingletonCreatorTestAccess.AllowAssetCreationDuringSuppression = false;
            EditorUi.Suppress = _previousEditorUiSuppress;
            AssetDatabaseBatchHelper.SaveAndRefreshIfNotBatching();

            CleanupAllKnownTestFolders();
            AssetPostprocessorDeferralTestAccess.Flush();
            yield return null;
        }

        public override void OneTimeTearDown()
        {
            try
            {
                base.OneTimeTearDown();
                CleanupAllKnownTestFolders();
            }
            finally
            {
                try
                {
                    _productionAssets?.Dispose();
                }
                finally
                {
                    ScriptableObjectSingleton<PersistentDirectorySettings>.ClearInstance();
                    ScriptableObjectSingleton<AttributeMetadataCache>.ClearInstance();
                    AssetPostprocessorDeferralTestAccess.Flush();
                }
            }
        }

        [UnityTest]
        public IEnumerator EnsureSingletonAssetsDoesNotCreateAssetsForExcludedTypes()
        {
            ScriptableObjectSingletonCreatorTestAccess.IncludeTestAssemblies = true;
            ScriptableObjectSingletonCreatorTestAccess.TypeFilter = null;
            // Ensure the metadata folder exists to prevent modal dialogs
            EnsureMetadataFolder();
            ScriptableObjectSingletonCreatorTestAccess.AllowAssetCreationDuringSuppression = true;

            List<string> beforePaths = new();
            foreach (Type type in SingletonTestTypes.WithExclusion)
            {
                string path = GetExpectedAssetPath(type);
                if (
                    !string.IsNullOrEmpty(path)
                    && AssetDatabase.LoadAssetAtPath<Object>(path) != null
                )
                {
                    beforePaths.Add(path);
                }
            }

            ScriptableObjectSingletonCreatorTestAccess.EnsureSingletonAssets();
            yield return null;
            AssetDatabaseBatchHelper.SaveAndRefreshIfNotBatching();
            yield return null;

            List<string> unexpectedlyCreated = new();
            foreach (Type type in SingletonTestTypes.WithExclusion)
            {
                string path = GetExpectedAssetPath(type);
                if (string.IsNullOrEmpty(path))
                {
                    continue;
                }

                if (beforePaths.Contains(path))
                {
                    continue;
                }

                Object asset = AssetDatabase.LoadAssetAtPath<Object>(path);
                if (asset != null)
                {
                    unexpectedlyCreated.Add($"{type.Name} at {path}");
                    _createdAssets.Add(path);
                }
            }

            Assert.IsEmpty(
                unexpectedlyCreated,
                $"EnsureSingletonAssets unexpectedly created assets for excluded types:\n{string.Join("\n", unexpectedlyCreated)}"
            );
        }

        [UnityTest]
        public IEnumerator EnsureSingletonAssetsRespectsExclusionEvenWithIncludeTestAssembliesTrue()
        {
            ScriptableObjectSingletonCreatorTestAccess.IncludeTestAssemblies = true;
            ScriptableObjectSingletonCreatorTestAccess.TypeFilter = null;
            // Ensure the metadata folder exists to prevent modal dialogs
            EnsureMetadataFolder();
            ScriptableObjectSingletonCreatorTestAccess.AllowAssetCreationDuringSuppression = true;
            yield return CleanupExistingTestSingletonAssets();

            ScriptableObjectSingletonCreatorTestAccess.EnsureSingletonAssets();
            yield return null;
            AssetDatabaseBatchHelper.SaveAndRefreshIfNotBatching();
            yield return null;

            List<string> createdAssets = new();
            foreach (Type type in SingletonTestTypes.WithExclusion)
            {
                string path = GetExpectedAssetPath(type);
                if (string.IsNullOrEmpty(path))
                {
                    continue;
                }

                Object asset = AssetDatabase.LoadAssetAtPath<Object>(path);
                if (asset != null)
                {
                    createdAssets.Add($"{type.Name} at {path}");
                    _createdAssets.Add(path);
                }
            }

            Assert.IsEmpty(
                createdAssets,
                $"Excluded singleton types should never have assets created, even with IncludeTestAssemblies=true:\n{string.Join("\n", createdAssets)}"
            );
        }

        [UnityTest]
        public IEnumerator TypeFilterBypassDoesNotOverrideExclusionAttribute()
        {
            ScriptableObjectSingletonCreatorTestAccess.IncludeTestAssemblies = true;
            ScriptableObjectSingletonCreatorTestAccess.TypeFilter = _ => true;
            // Ensure the metadata folder exists to prevent modal dialogs
            EnsureMetadataFolder();
            ScriptableObjectSingletonCreatorTestAccess.AllowAssetCreationDuringSuppression = true;
            yield return CleanupExistingTestSingletonAssets();

            ScriptableObjectSingletonCreatorTestAccess.EnsureSingletonAssets();
            yield return null;
            AssetDatabaseBatchHelper.SaveAndRefreshIfNotBatching();
            yield return null;

            List<string> createdAssets = new();
            foreach (Type type in SingletonTestTypes.WithExclusion)
            {
                string path = GetExpectedAssetPath(type);
                if (string.IsNullOrEmpty(path))
                {
                    continue;
                }

                Object asset = AssetDatabase.LoadAssetAtPath<Object>(path);
                if (asset != null)
                {
                    createdAssets.Add($"{type.Name} at {path}");
                    _createdAssets.Add(path);
                }
            }

            Assert.IsEmpty(
                createdAssets,
                $"Excluded singleton types should never be created even when TypeFilter returns true:\n{string.Join("\n", createdAssets)}"
            );
        }

        [UnityTest]
        public IEnumerator ResourcesFolderRemainsCleanAfterMultipleEnsureCalls()
        {
            // Ensure the metadata folder exists to prevent modal dialogs
            EnsureMetadataFolder();
            yield return CleanupExistingTestSingletonAssets();

            HashSet<string> initialAssetGuids = new(
                AssetDatabase.FindAssets("t:ScriptableObject", new[] { ResourcesRoot })
            );

            for (int i = 0; i < 3; ++i)
            {
                ScriptableObjectSingletonCreatorTestAccess.IncludeTestAssemblies = true;
                ScriptableObjectSingletonCreatorTestAccess.TypeFilter = null;
                ScriptableObjectSingletonCreatorTestAccess.AllowAssetCreationDuringSuppression =
                    true;
                ScriptableObjectSingletonCreatorTestAccess.EnsureSingletonAssets();
                yield return null;
                AssetDatabaseBatchHelper.SaveAndRefreshIfNotBatching();
                yield return null;
            }

            string[] finalGuids = AssetDatabase.FindAssets(
                "t:ScriptableObject",
                new[] { ResourcesRoot }
            );
            List<string> newAssets = new();
            foreach (string guid in finalGuids)
            {
                if (!initialAssetGuids.Contains(guid))
                {
                    string path = AssetDatabase.GUIDToAssetPath(guid);
                    foreach (Type type in SingletonTestTypes.WithExclusion)
                    {
                        if (path.Contains(type.Name))
                        {
                            newAssets.Add(path);
                            _createdAssets.Add(path);
                            break;
                        }
                    }
                }
            }

            Assert.IsEmpty(
                newAssets,
                $"Test singleton assets were unexpectedly created in Resources:\n{string.Join("\n", newAssets)}"
            );
        }

        [UnityTest]
        public IEnumerator TestTypesWithoutExclusionNotCreatedWhenIncludeTestAssembliesIsFalse()
        {
            ScriptableObjectSingletonCreatorTestAccess.IncludeTestAssemblies = false;
            ScriptableObjectSingletonCreatorTestAccess.TypeFilter = null;
            // Ensure the metadata folder exists to prevent modal dialogs
            EnsureMetadataFolder();
            ScriptableObjectSingletonCreatorTestAccess.AllowAssetCreationDuringSuppression = true;
            yield return CleanupExistingTestSingletonAssets();

            ScriptableObjectSingletonCreatorTestAccess.EnsureSingletonAssets();
            yield return null;
            AssetDatabaseBatchHelper.SaveAndRefreshIfNotBatching();
            yield return null;

            List<string> createdAssets = new();
            foreach (Type type in SingletonTestTypes.WithoutExclusion)
            {
                string path = GetExpectedAssetPath(type);
                if (string.IsNullOrEmpty(path))
                {
                    continue;
                }

                Object asset = AssetDatabase.LoadAssetAtPath<Object>(path);
                if (asset != null)
                {
                    createdAssets.Add($"{type.Name} at {path}");
                    _createdAssets.Add(path);
                }
            }

            Assert.IsEmpty(
                createdAssets,
                $"Test types should not be created when IncludeTestAssemblies=false (protected by IsTestType):\n{string.Join("\n", createdAssets)}"
            );
        }

        [Test]
        public void NoDuplicateFoldersExist(
            [ValueSource(nameof(DuplicateFolderTestCases))] DuplicateFolderTestCase testCase
        )
        {
            if (!AssetDatabase.IsValidFolder(testCase.ParentPath))
            {
                Assert.Pass(
                    $"Parent folder '{testCase.ParentPath}' doesn't exist - no duplicates possible."
                );
                return;
            }

            string[] subFolders = AssetDatabase.GetSubFolders(testCase.ParentPath);
            List<string> duplicates = new();
            string prefix = testCase.FolderBaseName + " ";

            foreach (string folder in subFolders)
            {
                string name = Path.GetFileName(folder);
                if (
                    name != null
                    && name.StartsWith(prefix, StringComparison.Ordinal)
                    && int.TryParse(name.Substring(prefix.Length), out _)
                )
                {
                    duplicates.Add(folder);
                }
            }

            if (0 < duplicates.Count)
            {
                Debug.LogWarning(
                    $"[SingletonAssetPollutionPreventionTests] Found {duplicates.Count} duplicate folder(s) in {testCase.ParentPath}. "
                        + $"Attempting cleanup before asserting."
                );

                foreach (string duplicate in duplicates)
                {
                    bool isDuplicatePollution = ProtectionTestHooks.TestIsKnownDuplicatePollution(
                        duplicate
                    );
                    bool isProtected = ProtectionTestHooks.TestIsProtectedPath(duplicate);
                    Debug.Log(
                        $"[SingletonAssetPollutionPreventionTests] Duplicate folder: {duplicate}, "
                            + $"IsDuplicatePollution={isDuplicatePollution}, IsProtected={isProtected}"
                    );
                }

                CleanupAllKnownTestFolders();
                AssetDatabaseBatchHelper.RefreshIfNotBatching();

                subFolders = AssetDatabase.GetSubFolders(testCase.ParentPath);
                duplicates.Clear();
                foreach (string folder in subFolders)
                {
                    string name = Path.GetFileName(folder);
                    if (
                        name != null
                        && name.StartsWith(prefix, StringComparison.Ordinal)
                        && int.TryParse(name.Substring(prefix.Length), out _)
                    )
                    {
                        duplicates.Add(folder);
                    }
                }
            }

            Assert.IsEmpty(
                duplicates,
                $"Found duplicate '{testCase.FolderBaseName}' folders in {testCase.ParentPath} that could not be cleaned up:\n"
                    + $"{string.Join("\n", duplicates)}\n\n"
                    + $"Description: {testCase.Description}\n"
                    + "This indicates a race condition or improper folder creation. "
                    + "Duplicates should have been cleaned by CleanupAllKnownTestFolders()."
            );
        }

        private IEnumerator CleanupExistingTestSingletonAssets()
        {
            foreach (Type type in SingletonTestTypes.All)
            {
                string path = GetExpectedAssetPath(type);
                if (!string.IsNullOrEmpty(path))
                {
                    DeleteAssetIfExists(path);
                    yield return null;
                }
            }

            AssetDatabaseBatchHelper.SaveAndRefreshIfNotBatching();
            yield return null;
        }

        private IEnumerator CleanupTestFolders()
        {
            string[] testFolders = new[]
            {
                ResourcesRoot + "/Tests/Nested/DeepPath",
                ResourcesRoot + "/Tests/Nested",
                ResourcesRoot + "/Tests/CreatorPath",
                ResourcesRoot + "/Tests",
                ResourcesRoot + "/CreatorTests/Collision",
                ResourcesRoot + "/CreatorTests/Retry",
                ResourcesRoot + "/CreatorTests/FileBlock",
                ResourcesRoot + "/CreatorTests/NoRetry",
                ResourcesRoot + "/CreatorTests",
                ResourcesRoot + "/CaseTest",
                ResourcesRoot + "/CustomPath",
                ResourcesRoot + "/SingleLevel",
                ResourcesRoot + "/Deep/Nested/Singletons",
                ResourcesRoot + "/Deep/Nested",
                ResourcesRoot + "/Deep",
                ResourcesRoot + "/Missing/Subfolder",
                ResourcesRoot + "/Missing",
            };

            foreach (string folder in testFolders)
            {
                TryDeleteEmptyFolder(folder);
                yield return null;
            }

            TryDeleteEmptyFolder(ResourcesRoot);
            yield return null;
        }

        public sealed class DuplicateFolderTestCase
        {
            public string ParentPath { get; }
            public string FolderBaseName { get; }
            public string Description { get; }

            public DuplicateFolderTestCase(
                string parentPath,
                string folderBaseName,
                string description
            )
            {
                ParentPath = parentPath;
                FolderBaseName = folderBaseName;
                Description = description;
            }

            public override string ToString()
            {
                return $"{FolderBaseName} in {ParentPath}";
            }
        }
    }
#endif
}
