using System.Reflection;
using System.Xml.Linq;
using ZB.MOM.WW.CBDD.Bson;
using ZB.MOM.WW.CBDD.Core;
using ZB.MOM.WW.CBDD.Core.Collections;
using ZB.MOM.WW.CBDD.Core.Indexing;
using ZB.MOM.WW.CBDD.Core.Storage;

namespace ZB.MOM.WW.CBDD.Tests;

public class ArchitectureFitnessTests
{
    private const string BsonProject = "src/CBDD.Bson/ZB.MOM.WW.CBDD.Bson.csproj";
    private const string CoreProject = "src/CBDD.Core/ZB.MOM.WW.CBDD.Core.csproj";
    private const string SourceGeneratorsProject = "src/CBDD.SourceGenerators/ZB.MOM.WW.CBDD.SourceGenerators.csproj";
    private const string FacadeProject = "src/CBDD/ZB.MOM.WW.CBDD.csproj";

    [Fact]
    public void Solution_DependencyGraph_ShouldRemainAcyclic_AndFollowLayerDirection()
    {
        var repoRoot = FindRepositoryRoot();
        var projectGraph = LoadSolutionProjectGraph(repoRoot);

        // Explicit layer rules
        projectGraph[BsonProject].ShouldBeEmpty();
        projectGraph[SourceGeneratorsProject].ShouldBeEmpty();
        projectGraph[CoreProject].ShouldBe(new[] { BsonProject });
        projectGraph[FacadeProject]
            .OrderBy(v => v, StringComparer.Ordinal)
            .ShouldBe(new[] { BsonProject, CoreProject, SourceGeneratorsProject }.OrderBy(v => v, StringComparer.Ordinal));

        // Source projects should not depend on tests.
        foreach (var kvp in projectGraph.Where(p => p.Key.StartsWith("src/", StringComparison.Ordinal)))
        {
            kvp.Value.Any(dep => dep.StartsWith("tests/", StringComparison.Ordinal))
                .ShouldBeFalse($"{kvp.Key} must not reference test projects.");
        }

        HasCycle(projectGraph)
            .ShouldBeFalse("Project references must remain acyclic.");
    }

    [Fact]
    public void HighLevelCollectionApi_ShouldNotExpandRawBsonReaderWriterSurface()
    {
        var lowLevelTypes = new[] { typeof(BsonSpanReader), typeof(BsonSpanWriter) };

        var collectionOffenders = typeof(DocumentCollection<,>)
            .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
            .Where(m => lowLevelTypes.Any(t => MethodUsesType(m, t)))
            .Select(m => m.Name)
            .Distinct()
            .OrderBy(n => n)
            .ToArray();

        collectionOffenders.ShouldBeEmpty();

        var dbContextOffenders = typeof(DocumentDbContext)
            .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
            .Where(m => lowLevelTypes.Any(t => MethodUsesType(m, t)))
            .Select(m => m.Name)
            .Distinct()
            .ToArray();

        dbContextOffenders.ShouldBeEmpty();
    }

    [Fact]
    public void CollectionAndIndexOrchestration_ShouldUseStoragePortInternally()
    {
        var targetTypes = new[]
        {
            typeof(DocumentCollection<>),
            typeof(DocumentCollection<,>),
            typeof(BTreeIndex),
            typeof(CollectionIndexManager<,>),
            typeof(CollectionSecondaryIndex<,>),
            typeof(VectorSearchIndex),
        };

        var fieldOffenders = targetTypes
            .SelectMany(t => t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
                .Where(f => f.FieldType == typeof(StorageEngine))
                .Select(f => $"{t.Name}.{f.Name}"))
            .OrderBy(v => v)
            .ToArray();

        fieldOffenders.ShouldBeEmpty("Collection/index orchestration should hold IStorageEngine instead of concrete StorageEngine.");
    }

    private static Dictionary<string, List<string>> LoadSolutionProjectGraph(string repoRoot)
    {
        var solutionPath = Path.Combine(repoRoot, "CBDD.slnx");
        var solutionDoc = XDocument.Load(solutionPath);

        var projects = solutionDoc
            .Descendants()
            .Where(e => e.Name.LocalName == "Project")
            .Select(e => e.Attribute("Path")?.Value)
            .Where(p => !string.IsNullOrWhiteSpace(p))
            .Select(p => NormalizePath(p!))
            .ToHashSet(StringComparer.Ordinal);

        var graph = projects.ToDictionary(
            p => p,
            _ => new List<string>(),
            StringComparer.Ordinal);

        foreach (var project in projects)
        {
            var projectFile = Path.Combine(repoRoot, project);
            var projectDoc = XDocument.Load(projectFile);
            var projectDir = Path.GetDirectoryName(projectFile)!;

            var refs = projectDoc
                .Descendants()
                .Where(e => e.Name.LocalName == "ProjectReference")
                .Select(e => e.Attribute("Include")?.Value)
                .Where(v => !string.IsNullOrWhiteSpace(v))
                .Select(v => v!.Replace('\\', '/'))
                .Select(v => NormalizePath(Path.GetRelativePath(repoRoot, Path.GetFullPath(Path.Combine(projectDir, v)))))
                .Where(projects.Contains)
                .Distinct(StringComparer.Ordinal)
                .OrderBy(v => v, StringComparer.Ordinal)
                .ToList();

            graph[project] = refs;
        }

        return graph;
    }

    private static bool HasCycle(Dictionary<string, List<string>> graph)
    {
        var state = graph.Keys.ToDictionary(k => k, _ => 0, StringComparer.Ordinal);

        foreach (var node in graph.Keys)
        {
            if (state[node] == 0 && Visit(node))
            {
                return true;
            }
        }

        return false;

        bool Visit(string node)
        {
            state[node] = 1; // visiting
            foreach (var dep in graph[node])
            {
                if (state[dep] == 1)
                {
                    return true;
                }

                if (state[dep] == 0 && Visit(dep))
                {
                    return true;
                }
            }

            state[node] = 2; // visited
            return false;
        }
    }

    private static bool MethodUsesType(MethodInfo method, Type forbidden)
    {
        if (TypeContains(method.ReturnType, forbidden))
        {
            return true;
        }

        return method.GetParameters().Any(p => TypeContains(p.ParameterType, forbidden));
    }

    private static bool TypeContains(Type inspected, Type forbidden)
    {
        if (inspected == forbidden)
        {
            return true;
        }

        if (inspected.HasElementType && inspected.GetElementType() is { } elementType && TypeContains(elementType, forbidden))
        {
            return true;
        }

        if (!inspected.IsGenericType)
        {
            return false;
        }

        return inspected.GetGenericArguments().Any(t => TypeContains(t, forbidden));
    }

    private static string FindRepositoryRoot()
    {
        var current = new DirectoryInfo(AppContext.BaseDirectory);
        while (current != null)
        {
            var solutionPath = Path.Combine(current.FullName, "CBDD.slnx");
            if (File.Exists(solutionPath))
            {
                return current.FullName;
            }

            current = current.Parent;
        }

        throw new InvalidOperationException("Unable to find repository root containing CBDD.slnx.");
    }

    private static string NormalizePath(string path)
        => path.Replace('\\', '/');
}
