using Xunit;

namespace KArtSell.ArchitectureTests;

public sealed class RepositoryRulesTests
{
    [Fact]
    public void Prohibited_source_patterns_are_not_introduced()
    {
        var repositoryRoot = FindRepositoryRoot();
        var sourceFiles = Directory.EnumerateFiles(Path.Combine(repositoryRoot, "src"), "*.cs", SearchOption.AllDirectories)
            .Where(x => !IsGeneratedOrTestOutput(x))
            .ToArray();

        // Check anti-patterns
        AssertNoPattern(sourceFiles, "IGenericRepository", "Generic repository is prohibited.");
        AssertNoPattern(sourceFiles, "IServiceProvider.GetService", "Service locator is prohibited.");
    }

    [Fact]
    public void DateTime_now_must_use_iclock_abstraction()
    {
        var repositoryRoot = FindRepositoryRoot();
        var sourceFiles = Directory.EnumerateFiles(Path.Combine(repositoryRoot, "src"), "*.cs", SearchOption.AllDirectories)
            .Where(x => !IsGeneratedOrTestOutput(x))
            .ToArray();

        var violations = sourceFiles
            .Where(path =>
            {
                var text = File.ReadAllText(path);
                return (text.Contains("DateTime.Now", StringComparison.Ordinal)
                        || text.Contains("DateTime.UtcNow", StringComparison.Ordinal)
                        || text.Contains("DateTimeOffset.UtcNow", StringComparison.Ordinal))
                       && !text.Contains("IClock", StringComparison.Ordinal);
            })
            .ToArray();

        Assert.True(violations.Length == 0,
            $"DateTime.Now/UtcNow must use IClock abstraction (not direct DateTime): {string.Join(", ", violations)}");
    }

    [Fact]
    public void Domain_files_do_not_reference_infrastructure_frameworks()
    {
        var root = FindRepositoryRoot();
        var domainFiles = Directory.EnumerateFiles(
            Path.Combine(root, "src"),
            "*.cs",
            SearchOption.AllDirectories)
            .Where(path => path.Contains(
                $"{Path.DirectorySeparatorChar}Domain{Path.DirectorySeparatorChar}",
                StringComparison.Ordinal))
            .ToArray();

        foreach (var prohibited in new[]
                 {
                     "using Dapper", "using Npgsql", "using FastEndpoints", "using Hangfire",
                     "HttpContext", "DbConnection", "IServiceCollection"
                 })
        {
            AssertNoPattern(domainFiles, prohibited,
                $"Domain cannot depend on infrastructure framework: {prohibited}.");
        }
    }

    [Fact]
    public void Sql_does_not_use_select_star_or_unqualified_signal_tables()
    {
        var repositoryRoot = FindRepositoryRoot();
        var files = Directory.EnumerateFiles(repositoryRoot, "*.*", SearchOption.AllDirectories)
            .Where(x => x.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)
                        || x.EndsWith(".sql", StringComparison.OrdinalIgnoreCase))
            .Where(x => !IsGeneratedOrTestOutput(x))
            .ToArray();

        var selectStar = files.Where(x =>
            File.ReadAllText(x).Contains("select *", StringComparison.OrdinalIgnoreCase)).ToArray();
        Assert.True(selectStar.Length == 0,
            "SELECT * is prohibited: " + string.Join(", ", selectStar));

        var unqualified = files.Where(x =>
        {
            var text = File.ReadAllText(x);
            return text.Contains(" from signal_decision", StringComparison.OrdinalIgnoreCase)
                   || text.Contains(" into signal_decision", StringComparison.OrdinalIgnoreCase)
                   || text.Contains(" from evidence_snapshot", StringComparison.OrdinalIgnoreCase);
        }).ToArray();
        Assert.True(unqualified.Length == 0,
            "SignalEngine SQL must be schema-qualified: " + string.Join(", ", unqualified));
    }

    [Fact]
    public void Every_module_endpoint_declares_roles_or_policies()
    {
        var root = FindRepositoryRoot();
        var endpoints = Directory.EnumerateFiles(
            Path.Combine(root, "src"),
            "Endpoint.cs",
            SearchOption.AllDirectories)
            .Where(path => path.Contains(
                $"{Path.DirectorySeparatorChar}Modules{Path.DirectorySeparatorChar}",
                StringComparison.Ordinal))
            .ToArray();

        var violations = endpoints.Where(path =>
        {
            var text = File.ReadAllText(path);
            return !text.Contains("Roles(", StringComparison.Ordinal)
                   && !text.Contains("Policies(", StringComparison.Ordinal);
        }).ToArray();

        Assert.True(violations.Length == 0,
            "Every module endpoint must declare Roles or Policies: " + string.Join(", ", violations));
    }

    [Fact]
    public void Role_declared_endpoints_must_not_allow_anonymous_access()
    {
        var root = FindRepositoryRoot();
        var endpoints = Directory.EnumerateFiles(
                Path.Combine(root, "src"),
                "*.cs",
                SearchOption.AllDirectories)
            .Where(path => Path.GetFileName(path).Contains("Endpoint", StringComparison.Ordinal))
            .ToArray();

        var violations = endpoints.Where(path =>
        {
            var text = File.ReadAllText(path);
            return text.Contains("Roles(", StringComparison.Ordinal)
                   && text.Contains("AllowAnonymous()", StringComparison.Ordinal)
                   && !text.Contains("/test/", StringComparison.Ordinal);
        }).ToArray();

        Assert.True(violations.Length == 0,
            "Role-declared endpoints must not also allow anonymous access: " + string.Join(", ", violations));
    }

    [Fact]
    public void Unapproved_reconciliation_endpoints_must_remain_unregistered()
    {
        var root = FindRepositoryRoot();
        var endpointFile = Path.Combine(root, "src", "KArtSell.Modules.ModelOperations", "PortfolioReconciliation", "Endpoints.cs");
        var text = File.ReadAllText(endpointFile);

        Assert.Equal(4, CountOccurrences(text, "[DontRegister]"));
        Assert.Equal(4, CountOccurrences(text, "AllowAnonymous()"));
    }

    [Fact]
    public void KIS_trade_endpoints_must_remain_unregistered_while_capability_is_off()
    {
        var root = FindRepositoryRoot();
        var endpointFile = Path.Combine(
            root, "src", "KArtSell.Modules.ModelOperations", "TradeExecution", "TradeEndpoints.cs");
        var text = File.ReadAllText(endpointFile);

        Assert.Equal(2, CountOccurrences(text, "[DontRegister]"));
        Assert.Equal(2, CountOccurrences(text, "AllowAnonymous()"));
    }

    [Fact]
    public void Job_run_repository_columns_exist_in_authoritative_baseline_schema()
    {
        var root = FindRepositoryRoot();
        var repository = File.ReadAllText(Path.Combine(
            root, "src", "KArtSell.BuildingBlocks", "Reliability", "DapperJobRunRepository.cs"));
        var schema = File.ReadAllText(Path.Combine(root, "db", "migrations", "0000_building_blocks.sql"));

        var requiredColumns = new[]
        {
            "job_run_id", "job_type", "job_version", "scope_key", "idempotency_key",
            "watermark", "app_version", "model_version", "config_version", "data_version",
            "contract_version", "status", "input_hash", "output_hash", "started_at",
            "heartbeat_at", "finished_at", "error_code", "trace_id"
        };

        var missingFromSchema = requiredColumns
            .Where(column => !schema.Contains(column, StringComparison.OrdinalIgnoreCase))
            .ToArray();
        Assert.Empty(missingFromSchema);

        var insertSection = repository[(repository.IndexOf("insert into", StringComparison.OrdinalIgnoreCase))..];
        var referencedInRepository = requiredColumns
            .Where(column => insertSection.Contains(column, StringComparison.OrdinalIgnoreCase))
            .ToArray();
        Assert.Equal(
            requiredColumns.OrderBy(x => x),
            referencedInRepository.OrderBy(x => x));
    }

    [Fact]
    public void Accidental_placeholder_files_are_not_committed()
    {
        var root = FindRepositoryRoot();
        var names = Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories)
            .Where(path => !IsGeneratedOrTestOutput(path))
            .Where(path => Path.GetFileName(path).Equals("testfile", StringComparison.OrdinalIgnoreCase)
                           || Path.GetFileName(path).EndsWith(".tmp", StringComparison.OrdinalIgnoreCase))
            .ToArray();

        Assert.True(names.Length == 0,
            "Placeholder files are prohibited: " + string.Join(", ", names));
    }

    [Fact]
    public void Aggregate_ids_are_unique_across_modules()
    {
        var root = FindRepositoryRoot();
        var aggregateIdFiles = Directory.EnumerateFiles(
            Path.Combine(root, "src"),
            "*.cs",
            SearchOption.AllDirectories)
            .Where(path => !IsGeneratedOrTestOutput(path))
            .ToArray();

        var aggregateIds = new Dictionary<string, List<string>>();

        foreach (var file in aggregateIdFiles)
        {
            var text = File.ReadAllText(file);

            // Match aggregate ID definitions: Guid("00000000-0000-0000-0000-...")
            var pattern = @"Guid\(\""[a-f0-9\-]{36}\""";
            var matches = System.Text.RegularExpressions.Regex.Matches(text, pattern);

            foreach (System.Text.RegularExpressions.Match match in matches)
            {
                var id = match.Value;
                if (!aggregateIds.TryGetValue(id, out var list))
                {
                    list = new List<string>();
                    aggregateIds[id] = list;
                }
                list.Add(file);
            }
        }

        var duplicates = aggregateIds
            .Where(kvp => kvp.Value.Count > 1)
            .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

        Assert.True(duplicates.Count == 0,
            duplicates.Count > 0
                ? $"Duplicate aggregate IDs detected: {string.Join("; ", duplicates.Select(d => $"{d.Key} in {string.Join(", ", d.Value)}"))}"
                : "No duplicate aggregate IDs found.");
    }

    private static void AssertNoPattern(IEnumerable<string> files, string pattern, string message)
    {
        var violations = files
            .Where(path => File.ReadAllText(path).Contains(pattern, StringComparison.Ordinal))
            .ToArray();
        Assert.True(violations.Length == 0, message + " " + string.Join(", ", violations));
    }

    private static int CountOccurrences(string text, string value) =>
        text.Split(value, StringSplitOptions.None).Length - 1;

    private static bool IsGeneratedOrTestOutput(string path)
        => path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
           || path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
           || path.Contains($"{Path.DirectorySeparatorChar}tests{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
           || path.Contains($"{Path.DirectorySeparatorChar}attachments{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
           || path.Contains($"{Path.DirectorySeparatorChar}research{Path.DirectorySeparatorChar}original{Path.DirectorySeparatorChar}", StringComparison.Ordinal);

    private static string FindRepositoryRoot()
    {
        var directory = new DirectoryInfo(AppContext.BaseDirectory);
        while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "Directory.Build.props")))
        {
            directory = directory.Parent;
        }

        return directory?.FullName
               ?? throw new InvalidOperationException("Repository root not found.");
    }
}
