using e_suite.Database.Core.Models;
using eSuite.Core.Miscellaneous;
using NUnit.Framework;
using System.Reflection;

namespace e_suite.API.Common.UnitTests;

[TestFixture]
public class PatchMapImplementationTests
{
    private Assembly _assembly = null!;

    private List<Type> _patchDtoTypes = null!;

    [SetUp]
    public void Setup()
    {
        _assembly = typeof(Patch<>).Assembly; // e_suite.API.Common

        _patchDtoTypes = _assembly
            .GetTypes()
            .Where(t => t.GetCustomAttribute<PatchesAttribute>() != null)
            .ToList();
    }

    [Test]
    public void CanDiscoverAllPatchMapAttributesInAssembly()
    {
        Assert.That(_patchDtoTypes, Is.Not.Empty, "No types with PatchMapAttribute were found.");
    }

    [Test]
    public void AllPatchMapTargetPropertyNamesMustResolveToRealProperties()
    {
        foreach (var dtoType in _patchDtoTypes)
        {
            var dtoProps = dtoType.GetProperties(BindingFlags.Public | BindingFlags.Instance);

            foreach (var dtoProp in dtoProps)
            {
                var mapAttr = dtoProp.GetCustomAttribute<PatchMapAttribute>();
                if (mapAttr == null)
                    continue;

                var targetPropName = mapAttr.TargetPropertyName;

                var exists = _assembly
                    .GetTypes()
                    .Any(t => t.GetProperty(
                        targetPropName,
                        BindingFlags.Public | BindingFlags.Instance) != null);

                Assert.That(
                    exists,
                    Is.True,
                    $"PatchMap on {dtoType.Name}.{dtoProp.Name} refers to unknown property '{targetPropName}' in assembly {_assembly.GetName().Name}.");
            }
        }
    }

    [Test]
    public void AllPatchDtoPropertiesMustHavePatchMapAttribute()
    {
        foreach (var dtoType in _patchDtoTypes)
        {
            var dtoProps = dtoType.GetProperties(BindingFlags.Public | BindingFlags.Instance);

            foreach (var dtoProp in dtoProps)
            {
                var hasPatchMap = dtoProp.GetCustomAttribute<PatchMapAttribute>() != null;

                Assert.That(
                    hasPatchMap,
                    Is.True,
                    $"Property '{dtoType.Name}.{dtoProp.Name}' is missing PatchMapAttribute.");
            }
        }
    }

    [Test]
    public void AllPatchMapAttributesMustMapToCompatibleTypes()
    {
        var allTypes = _assembly.GetTypes();

        foreach (var dtoType in _patchDtoTypes)
        {
            var dtoProps = dtoType.GetProperties(BindingFlags.Public | BindingFlags.Instance);

            foreach (var dtoProp in dtoProps)
            {
                var mapAttr = dtoProp.GetCustomAttribute<PatchMapAttribute>();
                if (mapAttr == null)
                    continue;

                var targetPropName = mapAttr.TargetPropertyName;

                // Find the target property anywhere in the assembly
                var targetProp = allTypes
                    .Select(t => t.GetProperty(targetPropName, BindingFlags.Public | BindingFlags.Instance))
                    .FirstOrDefault(p => p != null);

                Assert.That(
                    targetProp,
                    Is.Not.Null,
                    $"PatchMap on {dtoType.Name}.{dtoProp.Name} refers to unknown property '{targetPropName}'.");

                var dtoPropType = dtoProp.PropertyType;
                var targetPropType = targetProp!.PropertyType;

                // Nullable<T> → T compatibility
                if (dtoPropType.IsGenericType && dtoPropType.GetGenericTypeDefinition() == typeof(Nullable<>))
                    dtoPropType = Nullable.GetUnderlyingType(dtoPropType)!;

                if (targetPropType.IsGenericType && targetPropType.GetGenericTypeDefinition() == typeof(Nullable<>))
                    targetPropType = Nullable.GetUnderlyingType(targetPropType)!;

                // Navigation properties (GeneralIdRef → IGeneralId)
                var isDtoGeneralIdRef = dtoPropType == typeof(GeneralIdRef);
                var isTargetNavigation = typeof(IGeneralId).IsAssignableFrom(targetPropType);

                if (isDtoGeneralIdRef && isTargetNavigation)
                    continue; // valid navigation mapping

                // Scalar → scalar compatibility
                Assert.That(
                    targetPropType.IsAssignableFrom(dtoPropType),
                    Is.True,
                    $"PatchMap type mismatch: {dtoType.Name}.{dtoProp.Name} ({dtoProp.PropertyType.Name}) " +
                    $"cannot be assigned to target property {targetProp.DeclaringType!.Name}.{targetProp.Name} ({targetProp.PropertyType.Name}).");
            }
        }
    }

    [Test]
    public void NoPatchDtoMayMapMultiplePropertiesToTheSameTargetProperty()
    {
        foreach (var dtoType in _patchDtoTypes)
        {
            var dtoProps = dtoType.GetProperties(BindingFlags.Public | BindingFlags.Instance);

            // Collect all mappings: targetPropertyName → list of dto properties mapping to it
            var mapGroups = dtoProps
                .Select(p => new
                {
                    Property = p,
                    Attribute = p.GetCustomAttribute<PatchMapAttribute>()
                })
                .Where(x => x.Attribute != null)
                .GroupBy(x => x.Attribute!.TargetPropertyName)
                .ToList();

            foreach (var group in mapGroups)
            {
                if (group.Count() > 1)
                {
                    var offendingProps = string.Join(
                        ", ",
                        group.Select(g => $"{dtoType.Name}.{g.Property.Name}")
                    );

                    Assert.Fail(
                        $"Multiple properties in DTO '{dtoType.Name}' map to the same target property '{group.Key}': {offendingProps}"
                    );
                }
            }
        }
    }

    [Test]
    public void NavigationMappingsMustMapOnlyToIGeneralIdImplementations()
    {
        foreach (var dtoType in _patchDtoTypes)
        {
            var patchesAttr = dtoType.GetCustomAttribute<PatchesAttribute>();
            Assert.That(
                patchesAttr,
                Is.Not.Null,
                $"DTO '{dtoType.Name}' is missing required [Patches] attribute.");

            var targetType = patchesAttr!.TargetType;

            var dtoProps = dtoType.GetProperties(BindingFlags.Public | BindingFlags.Instance);

            foreach (var dtoProp in dtoProps)
            {
                var mapAttr = dtoProp.GetCustomAttribute<PatchMapAttribute>();
                if (mapAttr == null)
                    continue;

                // Only enforce for navigation DTO properties
                if (dtoProp.PropertyType != typeof(GeneralIdRef))
                    continue;

                var targetPropName = mapAttr.TargetPropertyName;

                var targetProp = targetType.GetProperty(
                    targetPropName,
                    BindingFlags.Public | BindingFlags.Instance);

                Assert.That(
                    targetProp,
                    Is.Not.Null,
                    $"PatchMap on {dtoType.Name}.{dtoProp.Name} refers to unknown property '{targetPropName}' on target type '{targetType.Name}'.");

                var targetPropType = targetProp!.PropertyType;

                // Unwrap nullable navigation types if needed
                if (targetPropType.IsGenericType &&
                    targetPropType.GetGenericTypeDefinition() == typeof(Nullable<>))
                {
                    targetPropType = Nullable.GetUnderlyingType(targetPropType)!;
                }

                Assert.That(
                    typeof(IGeneralId).IsAssignableFrom(targetPropType),
                    Is.True,
                    $"Navigation mapping error: {dtoType.Name}.{dtoProp.Name} (GeneralIdRef) maps to " +
                    $"{targetType.Name}.{targetProp.Name} ({targetProp.PropertyType.Name}), " +
                    $"which does not implement IGeneralId.");
            }
        }
    }

    [Test]
    public void PatchesAttributeMustReferenceAConcretePublicClass()
    {
        foreach (var dtoType in _patchDtoTypes)
        {
            var patchesAttr = dtoType.GetCustomAttribute<PatchesAttribute>();
            Assert.That(
                patchesAttr,
                Is.Not.Null,
                $"DTO '{dtoType.Name}' is missing required [Patches] attribute.");

            var targetType = patchesAttr!.TargetType;

            Assert.That(
                targetType.IsClass,
                Is.True,
                $"[Patches] on '{dtoType.Name}' must reference a class, but references '{targetType.Name}'.");

            Assert.That(
                !targetType.IsAbstract,
                Is.True,
                $"[Patches] on '{dtoType.Name}' references abstract type '{targetType.Name}', which cannot be patched.");

            Assert.That(
                targetType.IsPublic,
                Is.True,
                $"[Patches] on '{dtoType.Name}' references non‑public type '{targetType.Name}', which cannot be patched.");
        }
    }

    [Test]
    public void AllPatchMappedTargetPropertiesMustBeWritable()
    {
        foreach (var dtoType in _patchDtoTypes)
        {
            var patchesAttr = dtoType.GetCustomAttribute<PatchesAttribute>();
            Assert.That(
                patchesAttr,
                Is.Not.Null,
                $"DTO '{dtoType.Name}' is missing required [Patches] attribute.");

            var targetType = patchesAttr!.TargetType;

            var dtoProps = dtoType.GetProperties(BindingFlags.Public | BindingFlags.Instance);

            foreach (var dtoProp in dtoProps)
            {
                var mapAttr = dtoProp.GetCustomAttribute<PatchMapAttribute>();
                if (mapAttr == null)
                    continue;

                var targetPropName = mapAttr.TargetPropertyName;

                var targetProp = targetType.GetProperty(
                    targetPropName,
                    BindingFlags.Public | BindingFlags.Instance);

                Assert.That(
                    targetProp,
                    Is.Not.Null,
                    $"PatchMap on {dtoType.Name}.{dtoProp.Name} refers to unknown property '{targetPropName}' on target type '{targetType.Name}'.");

                Assert.That(
                    targetProp!.CanWrite,
                    Is.True,
                    $"PatchMap on {dtoType.Name}.{dtoProp.Name} refers to non-writable property '{targetProp.Name}' on target type '{targetType.Name}'.");
            }
        }
    }
}