using System.Reflection;
using System.Text.RegularExpressions;
using System.Xml.Linq;

namespace CashLineERP.Api.Tests;

public sealed class ArchitectureRulesQaTests
{
    private static readonly string RepoRoot = ResolveRepoRoot();

    private static readonly IReadOnlyDictionary<string, string[]> AllowedProjectReferences = new Dictionary<string, string[]>(
        StringComparer.OrdinalIgnoreCase)
    {
        ["CashLineERP.SharedKernel"] = [],
        ["CashLineERP.Domain"] = ["CashLineERP.SharedKernel"],
        ["CashLineERP.Contracts"] = [],
        ["CashLineERP.Application"] = ["CashLineERP.Contracts", "CashLineERP.Domain", "CashLineERP.SharedKernel"],
        ["CashLineERP.Infrastructure"] = ["CashLineERP.Application", "CashLineERP.Contracts", "CashLineERP.Domain", "CashLineERP.SharedKernel"],
        ["CashLineERP.Api"] = ["CashLineERP.Application", "CashLineERP.Contracts", "CashLineERP.Infrastructure"],
        ["CashLineERP.Workers"] = ["CashLineERP.Application", "CashLineERP.Infrastructure"]
    };

    private static readonly IReadOnlyDictionary<string, string[]> AllowedPackageReferences = new Dictionary<string, string[]>(
        StringComparer.OrdinalIgnoreCase)
    {
        ["CashLineERP.SharedKernel"] = [],
        ["CashLineERP.Domain"] = [],
        ["CashLineERP.Contracts"] = [],
        ["CashLineERP.Application"] = [],
        ["CashLineERP.Infrastructure"] =
        [
            "Microsoft.EntityFrameworkCore",
            "Microsoft.EntityFrameworkCore.Design",
            "Microsoft.EntityFrameworkCore.Sqlite",
            "Microsoft.EntityFrameworkCore.SqlServer"
        ],
        ["CashLineERP.Api"] = [],
        ["CashLineERP.Workers"] = ["Microsoft.Extensions.Hosting"]
    };

    private static readonly (string ProjectName, string RootNamespace, string ProjectDirectory)[] Projects =
    [
        ("CashLineERP.SharedKernel", "CashLineERP.SharedKernel", Path.Combine(RepoRoot, "src/backend/CashLineERP.SharedKernel")),
        ("CashLineERP.Domain", "CashLineERP.Domain", Path.Combine(RepoRoot, "src/backend/CashLineERP.Domain")),
        ("CashLineERP.Contracts", "CashLineERP.Contracts", Path.Combine(RepoRoot, "src/backend/CashLineERP.Contracts")),
        ("CashLineERP.Application", "CashLineERP.Application", Path.Combine(RepoRoot, "src/backend/CashLineERP.Application")),
        ("CashLineERP.Infrastructure", "CashLineERP.Infrastructure", Path.Combine(RepoRoot, "src/backend/CashLineERP.Infrastructure")),
        ("CashLineERP.Api", "CashLineERP.Api", Path.Combine(RepoRoot, "src/backend/CashLineERP.Api")),
        ("CashLineERP.Workers", "CashLineERP.Workers", Path.Combine(RepoRoot, "src/backend/CashLineERP.Workers"))
    ];

    [Fact]
    public void Layer_Dependency_Checks_Project_References_Follow_Architecture_Rules()
    {
        foreach (var project in Projects)
        {
            var csprojPath = Path.Combine(project.ProjectDirectory, $"{project.ProjectName}.csproj");
            var document = XDocument.Load(csprojPath);
            var actualReferences = document.Descendants("ProjectReference")
                .Select(node => NormalizeProjectReferenceName(node.Attribute("Include")?.Value))
                .Where(value => !string.IsNullOrWhiteSpace(value))
                .OrderBy(value => value, StringComparer.OrdinalIgnoreCase)
                .ToArray();

            var expectedReferences = AllowedProjectReferences[project.ProjectName]
                .OrderBy(value => value, StringComparer.OrdinalIgnoreCase)
                .ToArray();

            Assert.Equal(expectedReferences, actualReferences);
        }
    }

    [Fact]
    public void Package_Dependencies_Are_Restricted_By_Project_Layer()
    {
        foreach (var project in Projects)
        {
            var csprojPath = Path.Combine(project.ProjectDirectory, $"{project.ProjectName}.csproj");
            var document = XDocument.Load(csprojPath);
            var actualPackages = document.Descendants("PackageReference")
                .Select(node => node.Attribute("Include")?.Value ?? string.Empty)
                .Where(value => !string.IsNullOrWhiteSpace(value))
                .OrderBy(value => value, StringComparer.OrdinalIgnoreCase)
                .ToArray();

            var expectedPackages = AllowedPackageReferences[project.ProjectName]
                .OrderBy(value => value, StringComparer.OrdinalIgnoreCase)
                .ToArray();

            Assert.Equal(expectedPackages, actualPackages);
        }
    }

    [Fact]
    public void Naming_Conventions_Use_Project_Root_Namespaces_And_File_Aligned_Types()
    {
        foreach (var project in Projects)
        {
            var sourceFiles = Directory.GetFiles(project.ProjectDirectory, "*.cs", SearchOption.AllDirectories)
                .Where(path => !path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
                .Where(path => !path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
                .ToArray();

            if (sourceFiles.Length == 0)
            {
                continue;
            }

            foreach (var filePath in sourceFiles)
            {
                if (Path.GetFileName(filePath).Equals("Program.cs", StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }

                if (Path.GetFileName(filePath).Equals("GlobalUsings.cs", StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }

                var source = File.ReadAllText(filePath);
                var relativePath = Path.GetRelativePath(RepoRoot, filePath);

                var namespaceMatch = Regex.Match(source, @"namespace\s+([A-Za-z0-9_.]+)\s*(?:;|\{)");
                Assert.True(namespaceMatch.Success, $"Expected file '{relativePath}' to declare a file-scoped namespace.");
                Assert.StartsWith(
                    project.RootNamespace,
                    namespaceMatch.Groups[1].Value,
                    StringComparison.Ordinal);

                var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(filePath);
                var typeMatches = Regex.Matches(
                    source,
                    @"\b(?:public|internal)\s+(?:sealed\s+|static\s+|abstract\s+|partial\s+)?(?:class|record|interface|enum|struct)\s+([A-Za-z_][A-Za-z0-9_]*)");

                if (typeMatches.Count == 0)
                {
                    continue;
                }

                Assert.Contains(typeMatches.Select(match => match.Groups[1].Value), typeName =>
                    string.Equals(typeName, fileNameWithoutExtension, StringComparison.Ordinal));
            }
        }
    }

    [Fact]
    public void Layer_Dependency_Checks_Runtime_Assemblies_Do_Not_Reference_Forbidden_CashLine_Layers()
    {
        var assemblyPaths = Projects
            .Select(project => Path.Combine(project.ProjectDirectory, "bin/Debug/net8.0", $"{project.ProjectName}.dll"))
            .Where(File.Exists)
            .ToArray();

        Assert.NotEmpty(assemblyPaths);

        foreach (var assemblyPath in assemblyPaths)
        {
            var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath);
            var project = Projects.Single(entry => string.Equals(entry.ProjectName, assemblyName, StringComparison.OrdinalIgnoreCase));
            var sourceFiles = Directory.GetFiles(project.ProjectDirectory, "*.cs", SearchOption.AllDirectories)
                .Where(path => !path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
                .Where(path => !path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
                .ToArray();

            if (sourceFiles.Length == 0)
            {
                continue;
            }

            var referencedCashLineAssemblies = Assembly.LoadFrom(assemblyPath)
                .GetReferencedAssemblies()
                .Select(reference => reference.Name)
                .Where(name => name is not null && name.StartsWith("CashLineERP.", StringComparison.Ordinal))
                .Cast<string>()
                .OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
                .ToArray();

            var allowedReferences = GetAllowedTransitiveReferences(assemblyName);

            Assert.All(referencedCashLineAssemblies, reference =>
                Assert.Contains(reference, allowedReferences, StringComparer.OrdinalIgnoreCase));
        }
    }

    private static string ResolveRepoRoot()
    {
        var directory = new DirectoryInfo(AppContext.BaseDirectory);

        while (directory is not null)
        {
            if (Directory.Exists(Path.Combine(directory.FullName, "src", "backend"))
                && Directory.Exists(Path.Combine(directory.FullName, "tests")))
            {
                return directory.FullName;
            }

            directory = directory.Parent;
        }

        throw new InvalidOperationException("Unable to locate repository root for architecture tests.");
    }

    private static string NormalizeProjectReferenceName(string? includePath)
    {
        if (string.IsNullOrWhiteSpace(includePath))
        {
            return string.Empty;
        }

        var normalizedPath = includePath.Replace('\\', Path.DirectorySeparatorChar);
        return Path.GetFileNameWithoutExtension(normalizedPath);
    }

    private static IReadOnlyCollection<string> GetAllowedTransitiveReferences(string projectName)
    {
        var collected = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var queue = new Queue<string>(AllowedProjectReferences[projectName]);

        while (queue.Count > 0)
        {
            var next = queue.Dequeue();
            if (!collected.Add(next))
            {
                continue;
            }

            if (!AllowedProjectReferences.TryGetValue(next, out var nested))
            {
                continue;
            }

            foreach (var entry in nested)
            {
                queue.Enqueue(entry);
            }
        }

        return collected;
    }
}
