using System.Reflection;
using System.Xml.Linq;

namespace ZB.MOM.WW.ScadaBridge.Commons.Tests;

public class ArchitecturalConstraintTests
{
    private static readonly Assembly CommonsAssembly = typeof(ZB.MOM.WW.ScadaBridge.Commons.Types.RetryPolicy).Assembly;

    private static string GetCsprojPath()
    {
        // Walk up from test output to find the Commons csproj
        var dir = new DirectoryInfo(AppContext.BaseDirectory);
        while (dir != null)
        {
            var candidate = Path.Combine(dir.FullName, "src", "ZB.MOM.WW.ScadaBridge.Commons", "ZB.MOM.WW.ScadaBridge.Commons.csproj");
            if (File.Exists(candidate))
                return candidate;
            dir = dir.Parent;
        }
        throw new InvalidOperationException("Could not find ZB.MOM.WW.ScadaBridge.Commons.csproj");
    }

    private static string GetCommonsSourceDirectory()
    {
        var csproj = GetCsprojPath();
        return Path.GetDirectoryName(csproj)!;
    }

    [Fact]
    public void Csproj_ShouldNotHaveForbiddenPackageReferences()
    {
        var csprojPath = GetCsprojPath();
        var doc = XDocument.Load(csprojPath);
        var ns = doc.Root!.GetDefaultNamespace();

        var forbiddenPrefixes = new[] { "Akka.", "Microsoft.AspNetCore.", "Microsoft.EntityFrameworkCore." };

        var packageRefs = doc.Descendants(ns + "PackageReference")
            .Select(e => e.Attribute("Include")?.Value)
            .Where(v => v != null)
            .ToList();

        foreach (var pkg in packageRefs)
        {
            foreach (var prefix in forbiddenPrefixes)
            {
                Assert.False(pkg!.StartsWith(prefix, StringComparison.OrdinalIgnoreCase),
                    $"Commons has forbidden package reference: {pkg}");
            }
        }
    }

    [Fact]
    public void Assembly_ShouldNotReferenceForbiddenAssemblies()
    {
        var forbiddenPrefixes = new[] { "Akka.", "Microsoft.AspNetCore.", "Microsoft.EntityFrameworkCore." };

        var referencedAssemblies = CommonsAssembly.GetReferencedAssemblies();

        foreach (var asmName in referencedAssemblies)
        {
            foreach (var prefix in forbiddenPrefixes)
            {
                Assert.False(asmName.Name!.StartsWith(prefix, StringComparison.OrdinalIgnoreCase),
                    $"Commons references forbidden assembly: {asmName.Name}");
            }
        }
    }

    [Fact]
    public void Commons_ShouldNotContainServiceOrActorImplementations()
    {
        // Heuristic: class has > 3 public methods that are neither constructors,
        // property accessors, common Object overrides, nor interface-implementation
        // methods (a dictionary wrapper exposing ContainsKey/TryGetValue/GetEnumerator
        // via IReadOnlyDictionary isn't a service — that's just the interface).
        var types = CommonsAssembly.GetTypes()
            .Where(t => t.IsClass && !t.IsAbstract && !t.IsInterface);

        foreach (var type in types)
        {
            var interfaceMethodNames = type.GetInterfaces()
                .SelectMany(i => i.GetMethods())
                .Select(m => m.Name)
                .ToHashSet(StringComparer.Ordinal);

            var publicMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
                .Where(m => !m.IsSpecialName) // excludes property getters/setters
                .Where(m => !m.Name.StartsWith("<")) // excludes compiler-generated
                .Where(m => m.Name != "ToString" && m.Name != "GetHashCode" &&
                            m.Name != "Equals" && m.Name != "Deconstruct" &&
                            m.Name != "PrintMembers" && m.Name != "GetType")
                .Where(m => !interfaceMethodNames.Contains(m.Name))
                .ToList();

            Assert.True(publicMethods.Count <= 3,
                $"Type {type.FullName} has {publicMethods.Count} public non-interface methods ({string.Join(", ", publicMethods.Select(m => m.Name))}), which suggests it may contain service/actor logic");
        }
    }

    [Fact]
    public void SourceFiles_ShouldNotContainToLocalTime()
    {
        var sourceDir = GetCommonsSourceDirectory();
        var csFiles = Directory.GetFiles(sourceDir, "*.cs", SearchOption.AllDirectories);

        foreach (var file in csFiles)
        {
            var content = File.ReadAllText(file);
            Assert.DoesNotContain("ToLocalTime()", content);
        }
    }

    [Fact]
    public void AllEntities_ShouldHaveNoEfAttributes()
    {
        var efAttributeNames = new HashSet<string>
        {
            "KeyAttribute", "ForeignKeyAttribute", "TableAttribute",
            "ColumnAttribute", "RequiredAttribute", "MaxLengthAttribute",
            "StringLengthAttribute", "DatabaseGeneratedAttribute",
            "NotMappedAttribute", "IndexAttribute", "InversePropertyAttribute"
        };

        var entityTypes = CommonsAssembly.GetTypes()
            .Where(t => t.IsClass && !t.IsAbstract && t.Namespace != null
                        && t.Namespace.Contains(".Entities."));

        foreach (var type in entityTypes)
        {
            foreach (var attr in type.GetCustomAttributes(true))
            {
                Assert.DoesNotContain(attr.GetType().Name, efAttributeNames);
            }

            foreach (var prop in type.GetProperties())
            {
                foreach (var attr in prop.GetCustomAttributes(true))
                {
                    Assert.False(efAttributeNames.Contains(attr.GetType().Name),
                        $"{type.Name}.{prop.Name} has EF attribute {attr.GetType().Name}");
                }
            }
        }
    }

    [Fact]
    public void DebugStreamEvent_ShouldNotExist()
    {
        // DebugStreamEvent was removed when debug streaming moved from ClusterClient to gRPC.
        // Events now flow via SiteStreamManager → StreamRelayActor → gRPC channel.
        var type = CommonsAssembly.GetTypes().FirstOrDefault(t => t.Name == "DebugStreamEvent");
        Assert.Null(type);
    }

    [Fact]
    public void AllEnums_ShouldBeSingularNamed()
    {
        var enums = CommonsAssembly.GetTypes().Where(t => t.IsEnum);

        foreach (var enumType in enums)
        {
            var name = enumType.Name;
            // Singular names should not end with 's' (except words ending in 'ss' or 'us' which are singular)
            Assert.False(name.EndsWith("s") && !name.EndsWith("ss") && !name.EndsWith("us"),
                $"Enum {name} appears to be plural");
        }
    }

    [Fact]
    public void AllMessageTypes_ShouldBeRecords()
    {
        var messageTypes = CommonsAssembly.GetTypes()
            .Where(t => t.Namespace != null
                        && t.Namespace.Contains(".Messages.")
                        && !t.IsEnum && !t.IsInterface
                        && !(t.IsAbstract && t.IsSealed) // exclude static classes (utilities)
                        && !t.Name.StartsWith("<") // exclude compiler-generated types
                        && (t.IsClass || (t.IsValueType && !t.IsPrimitive)));

        foreach (var type in messageTypes)
        {
            var cloneMethod = type.GetMethod("<Clone>$", BindingFlags.Public | BindingFlags.Instance);
            Assert.True(cloneMethod != null,
                $"{type.FullName} in Messages namespace should be a record type");
        }
    }

    [Fact]
    public void KeyEntities_ShouldHaveDateTimeOffsetTimestamps()
    {
        // Spot-check key entity timestamp properties
        var deploymentRecord = CommonsAssembly.GetType("ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment.DeploymentRecord");
        Assert.NotNull(deploymentRecord);
        Assert.Equal(typeof(DateTimeOffset), deploymentRecord!.GetProperty("DeployedAt")!.PropertyType);
        Assert.Equal(typeof(DateTimeOffset?), deploymentRecord.GetProperty("CompletedAt")!.PropertyType);

        var artifactRecord = CommonsAssembly.GetType("ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment.SystemArtifactDeploymentRecord");
        Assert.NotNull(artifactRecord);
        Assert.Equal(typeof(DateTimeOffset), artifactRecord!.GetProperty("DeployedAt")!.PropertyType);

        var auditEntry = CommonsAssembly.GetType("ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit.AuditLogEntry");
        Assert.NotNull(auditEntry);
        Assert.Equal(typeof(DateTimeOffset), auditEntry!.GetProperty("Timestamp")!.PropertyType);
    }
}
