using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.TypeSpec.Generator.Input;
using Microsoft.TypeSpec.Generator.Primitives;
using Microsoft.TypeSpec.Generator.Providers;
using Microsoft.TypeSpec.Generator.Tests.Common;
using Microsoft.TypeSpec.Generator.Tests.Providers.NamedTypeSymbolProviders;
using NUnit.Framework;
using static Microsoft.TypeSpec.Generator.Snippets.Snippet;

namespace Microsoft.TypeSpec.Generator.Tests.Providers
{
    public class CanonicalTypeProviderTests
    {
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor.
        private NamedSymbol _namedSymbol;
        private TestTypeProvider _typeProvider;
        private Compilation _compilation;
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor.
        private const string Name = "TestName";
        private const string Ns = "Sample.Models";

        [SetUp]
        public async Task Setup()
        {
            _namedSymbol = new NamedSymbol(name: Name, @namespace: Ns);
            _compilation = CompilationHelper.LoadCompilation([_namedSymbol, new PropertyType()]);
            _ = CompilationHelper.GetSymbol(_compilation.Assembly.Modules.First().GlobalNamespace, Name);
            await MockHelpers.LoadMockGeneratorAsync(compilation: () => Task.FromResult(_compilation));
            _typeProvider = new TestTypeProvider(Name, Ns);
        }

        [Test]
        public void ValidateModifiers()
        {
            var modifiers = _typeProvider.CanonicalView.DeclarationModifiers;
            Assert.IsTrue(modifiers.HasFlag(TypeSignatureModifiers.Internal | TypeSignatureModifiers.Partial | TypeSignatureModifiers.Class));
        }

        [Test]
        public void ValidateName()
        {
            Assert.AreEqual(_typeProvider.Name, _typeProvider.CanonicalView.Name);
        }

        [Test]
        public void ValidateNamespace()
        {
            Assert.AreEqual(_typeProvider.Type.Namespace, _typeProvider.CanonicalView.Type.Namespace);
        }

        [Test]
        public void ValidateProperties()
        {
            // Call EnsureBuilt to simulate the real build pipeline where members are cached
            // without customization filtering before the canonical view is accessed.
            _typeProvider.EnsureBuilt();
            // customization code provides 5 properties:
            // - public int IntProperty { get; set; }
            // - public string StringProperty { get; }
            // - public string InternalStringProperty { get; }
            // - public PropertyType PropertyTypeProperty { get; set; }
            // - public string NullWireInfoProperty { get; set; }
            // generated code provides 3 properties:
            // - public int IntProperty { get; set; }
            // - public string SpecProperty { get; }
            // - public string NullWireInfoProperty { get; set; }
            // therefore the CanonicalType should have 6 properties:
            // - public int IntProperty { get; set; } (from customization code)
            // - public string StringProperty { get; } (from customization code)
            // - public string NullWireInfoProperty { get; set; } (from customization code)
            // - public string SpecProperty { get; } (from generated code)
            // - public string InternalStringProperty { get; } (from customization code)
            // - public PropertyType PropertyTypeProperty { get; set; } (from customization code)
            Dictionary<string, PropertyProvider> properties = _typeProvider.CanonicalView.Properties.ToDictionary(p => p.Name);
            Assert.AreEqual(6, properties.Count);
            Assert.AreEqual(5, _typeProvider.CustomCodeView!.Properties.Count);

            // Validate the exact order
            // IntProperty
            // StringProperty
            // InternalStringProperty
            // PropertyTypeProperty
            // NullWireInfoProperty

            var propertiesList = properties.Values.ToList();
            Assert.AreEqual(6, propertiesList.Count);
            // Property 0: IntProperty
            {
                var actual = propertiesList[0];
                Assert.AreEqual("IntProperty", actual.Name);
                var expected = _namedSymbol.Properties.First(p => p.Name == "IntProperty");
                ValidatePropertyAttributes(expected, actual);
                Assert.IsNotNull(actual.WireInfo);
                Assert.AreEqual("intProperty", actual.WireInfo!.SerializedName);
            }

            // Property 1: StringProperty
            {
                var actual = propertiesList[1];
                Assert.AreEqual("StringProperty", actual.Name);
                var expected = _namedSymbol.Properties.First(p => p.Name == "StringProperty");
                ValidatePropertyAttributes(expected, actual);
                Assert.IsNotNull(actual.WireInfo);
                Assert.AreEqual("stringProperty", actual.WireInfo!.SerializedName);
            }

            // Property 2: NullWireInfoProperty
            {
                var actual = propertiesList[2];
                Assert.AreEqual("NullWireInfoProperty", actual.Name);
                var expected = _namedSymbol.Properties.First(p => p.Name == "NullWireInfoProperty");
                ValidatePropertyAttributes(expected, actual);
                Assert.IsNotNull(actual.WireInfo);
                Assert.AreEqual("NullWireInfoProperty", actual.WireInfo!.SerializedName);
            }

            // Property 3: SpecProperty
            {
                var actual = propertiesList[3];
                Assert.AreEqual("SpecProperty", actual.Name);
                Assert.IsFalse(_namedSymbol.Properties.Any(p => p.Name == "SpecProperty"));
                Assert.IsNotNull(actual.WireInfo);
                Assert.AreEqual("specProperty", actual.WireInfo!.SerializedName);
            }

            // Property 4: InternalStringProperty
            {
                var actual = propertiesList[4];
                Assert.AreEqual("InternalStringProperty", actual.Name);
                var expected = _namedSymbol.Properties.First(p => p.Name == "InternalStringProperty");
                ValidatePropertyAttributes(expected, actual);
                Assert.IsNull(actual.WireInfo);
            }

            // Property 5: PropertyTypeProperty
            {
                var actual = propertiesList[5];
                Assert.AreEqual("PropertyTypeProperty", actual.Name);
                var expected = _namedSymbol.Properties.First(p => p.Name == "PropertyTypeProperty");
                ValidatePropertyAttributes(expected, actual);
                Assert.IsNull(actual.WireInfo);
            }
        }

        private void ValidatePropertyAttributes(PropertyProvider expected, PropertyProvider actual)
        {
            Assert.AreEqual(expected.Name, actual.Name,
                $"Name mismatch for property {expected.Name}");

            Assert.IsNotNull(actual.Description,
                $"Description is null for property {expected.Name}");

            if (expected.WireInfo != null)
            {
                Assert.AreEqual(
                    $"{expected.Description}.",
                    actual.Description!.ToString(),
                    $"Description mismatch for property {expected.Name}");
            }

            Assert.AreEqual(expected.Modifiers, actual.Modifiers,
                $"Modifiers mismatch for property {expected.Name}");

            Assert.AreEqual(expected.Type, actual.Type,
                $"Type mismatch for property {expected.Name}");

            Assert.AreEqual(expected.Body.GetType(), actual.Body.GetType(),
                $"Body type mismatch for property {expected.Name}");

            Assert.AreEqual(expected.Body.HasSetter, actual.Body.HasSetter,
                $"HasSetter mismatch for property {expected.Name}");
        }

        [Test]
        public void ValidateMethods()
        {
            // Call EnsureBuilt to simulate the real build pipeline where members are cached
            // without customization filtering before the canonical view is accessed.
            _typeProvider.EnsureBuilt();

            // customization code provides a method:
            // - public virtual Task<int> Method1(int intParam)
            // generated code provides three methods:
            // - internal virtual Task<string> Method1(int p)
            // - public virtual Task<string> Method1(string strParam)
            // - public virtual Task Method2(float floatParam)
            // therefore the CanonicalType should have three methods:
            // - public virtual Task<int> Method1(int intParam)
            // - public virtual Task<string> Method1(string strParam)
            // - public virtual Task Method2(float floatParam)
            var methods = _typeProvider.CanonicalView.Methods;

            Assert.AreEqual(4, methods.Count);
            Assert.AreEqual(2, _typeProvider.CustomCodeView!.Methods.Count);

            // the first should be public virtual Task<string> Method1(string strParam)
            var first = methods[0].Signature;
            Assert.AreEqual("Method1", first.Name);
            Assert.AreEqual(MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual, first.Modifiers);
            Assert.AreEqual(new CSharpType(typeof(Task<string>)), first.ReturnType);
            Assert.AreEqual(1, first.Parameters.Count);
            Assert.AreEqual("strParam", first.Parameters[0].Name);
            Assert.AreEqual(new CSharpType(typeof(string)), first.Parameters[0].Type);

            // the second should be public virtual Task Method2(float floatParam)
            var second = methods[1].Signature;
            Assert.AreEqual("Method2", second.Name);
            Assert.AreEqual(MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual, second.Modifiers);
            Assert.AreEqual(new CSharpType(typeof(Task)), second.ReturnType);
            Assert.AreEqual(1, second.Parameters.Count);
            Assert.AreEqual("floatParam", second.Parameters[0].Name);
            Assert.AreEqual(new CSharpType(typeof(float)), second.Parameters[0].Type);

            // the third should be public virtual Task<int> Method1(int intParam)
            var third = methods[2].Signature;
            Assert.AreEqual("Method1", third.Name);
            Assert.AreEqual(MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual, third.Modifiers);
            Assert.AreEqual(new CSharpType(typeof(Task<int>)), third.ReturnType);
            Assert.AreEqual(1, third.Parameters.Count);
            Assert.AreEqual("intParam", third.Parameters[0].Name);
            Assert.AreEqual(new CSharpType(typeof(int)), third.Parameters[0].Type);

            // the fourth should be ValueTask DisposeAsync()
            var fourth = methods[3].Signature;
            Assert.AreEqual("DisposeAsync", fourth.Name);
            Assert.AreEqual("global::System.IAsyncDisposable.DisposeAsync", fourth.FullMethodName);
            Assert.AreEqual(MethodSignatureModifiers.Async, fourth.Modifiers);
            Assert.AreEqual(new CSharpType(typeof(ValueTask)), fourth.ReturnType);
            Assert.AreEqual(0, fourth.Parameters.Count);
        }

        [Test]
        public void ValidateConstructors()
        {
            // Call EnsureBuilt to simulate the real build pipeline where members are cached
            // without customization filtering before the canonical view is accessed.
            _typeProvider.EnsureBuilt();

            // customization code provides 1 constructor:
            // - public TestName(int intParam)
            // generated code provides 2 constructors:
            // - internal TestName(int intParam) (customized by the NamedSymbol)
            // - internal TestName() (not customized)
            // therefore the CanonicalType should have 2 constructors:
            // - public TestName(int intParam) (from customization code)
            // - internal TestName() (from generated code)
            var constructors = _typeProvider.CanonicalView.Constructors;

            Assert.AreEqual(2, constructors.Count);

            // the first should be the non-customized parameterless constructor
            var first = constructors[0].Signature;
            Assert.AreEqual(0, first.Parameters.Count);
            Assert.AreEqual(MethodSignatureModifiers.Internal, first.Modifiers);

            // the second should be the custom constructor with int param
            var second = constructors[1].Signature;
            Assert.AreEqual(1, second.Parameters.Count);
            Assert.AreEqual("intParam", second.Parameters[0].Name);
            Assert.AreEqual(new CSharpType(typeof(int)), second.Parameters[0].Type);
            Assert.AreEqual(MethodSignatureModifiers.Public, second.Modifiers);
        }

        [Test]
        public void ValidateFields()
        {
            // Call EnsureBuilt to simulate the real build pipeline where members are cached
            // without customization filtering before the canonical view is accessed.
            _typeProvider.EnsureBuilt();

            // customization code provides 4 fields:
            // - public int IntField
            // - private string StringField
            // - internal double DoubleField
            // - public static float FloatField
            // generated code provides 2 fields:
            // - private int IntField (customized by the NamedSymbol)
            // - private string GeneratedOnlyField (not customized)
            // therefore the CanonicalType should have 5 fields:
            // - private string GeneratedOnlyField (from generated code)
            // - public int IntField (from customization code)
            // - private string StringField (from customization code)
            // - internal double DoubleField (from customization code)
            // - public static float FloatField (from customization code)
            var fields = _typeProvider.CanonicalView.Fields;

            Assert.AreEqual(5, fields.Count);
            var fieldNames = fields.Select(f => f.Name).ToList();
            Assert.Contains("GeneratedOnlyField", fieldNames);
            Assert.Contains("IntField", fieldNames);
            Assert.Contains("StringField", fieldNames);
            Assert.Contains("DoubleField", fieldNames);
            Assert.Contains("FloatField", fieldNames);

            // IntField should appear only once (not duplicated)
            Assert.AreEqual(1, fields.Count(f => f.Name == "IntField"));
        }

        [Test]
        public void ValidatePropertiesWithNoSpecProperties()
        {
            // Call EnsureBuilt to simulate the real build pipeline where members are cached
            // without customization filtering before the canonical view is accessed.
            var noSpecProvider = new NoSpecTypeProvider(Name, Ns);
            noSpecProvider.EnsureBuilt();

            // customization code provides 5 properties:
            // - public int IntProperty { get; set; }
            // - public string StringProperty { get; }
            // - public string InternalStringProperty { get; }
            // - public PropertyType PropertyTypeProperty { get; set; }
            // - public string NullWireInfoProperty { get; set; }
            // generated code provides 2 properties:
            // - public int IntProperty { get; set; } (customized by the NamedSymbol)
            // - public string SpecProperty { get; } (not customized)
            // therefore the CanonicalType should have 6 properties:
            // - public string SpecProperty { get; } (from generated code)
            // - public int IntProperty { get; set; } (from customization code)
            // - public string StringProperty { get; } (from customization code)
            // - public string InternalStringProperty { get; } (from customization code)
            // - public PropertyType PropertyTypeProperty { get; set; } (from customization code)
            // - public string NullWireInfoProperty { get; set; } (from customization code)
            var properties = noSpecProvider.CanonicalView.Properties;

            Assert.AreEqual(6, properties.Count);

            // IntProperty should appear only once (not duplicated)
            Assert.AreEqual(1, properties.Count(p => p.Name == "IntProperty"));

            var propertyNames = properties.Select(p => p.Name).ToList();
            Assert.Contains("SpecProperty", propertyNames);
            Assert.Contains("IntProperty", propertyNames);
            Assert.Contains("StringProperty", propertyNames);
            Assert.Contains("InternalStringProperty", propertyNames);
            Assert.Contains("PropertyTypeProperty", propertyNames);
            Assert.Contains("NullWireInfoProperty", propertyNames);
        }

        [Test]
        public async Task TestPropertyDocs()
        {
            await MockHelpers.LoadMockGeneratorAsync(compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());
            Dictionary<string, PropertyProvider> properties = _typeProvider.CanonicalView.Properties.ToDictionary(p => p.Name);
            Assert.AreEqual(4, properties.Count);

            var intProperty = properties.TryGetValue("IntProperty", out var intProp) ? intProp : null;
            Assert.IsNotNull(intProperty);

            // validate the description is the original description
            Assert.AreEqual("Description for IntProperty", intProperty!.Description?.ToString());

            var customProperty = properties.TryGetValue("CustomProperty", out var customProp) ? customProp : null;
            Assert.IsNotNull(customProp);

            // validate the description is empty since it is not in the spec
            Assert.AreEqual("Custom summary for CustomProperty", customProp!.Description?.ToString());
        }


        private class TestTypeProvider : TypeProvider
        {
            private readonly string _name;
            private readonly string _namespace;
            public TestTypeProvider(string name, string ns)
                : base(GetSpecType())
            {
                _name = name;
                _namespace = ns;
            }

            private static InputType GetSpecType()
            {
                InputModelProperty[] properties =
                [
                    InputFactory.Property("IntProperty", InputPrimitiveType.Int32, wireName: "intProperty"),
                    InputFactory.Property("StringProperty", InputPrimitiveType.String, wireName: "stringProperty"),
                    new InputModelProperty("NullWireInfoProperty", null, null, InputPrimitiveType.String, false, false, null, false, "NullWireInfoProperty", false, false, null, new InputSerializationOptions())
                ];
                return InputFactory.Model("TestName", "Sample.Models", properties: properties);
            }
            protected override string BuildRelativeFilePath() => "NamedSymbol";

            protected override string BuildName() => _name;

            protected override string BuildNamespace() => _namespace;

            protected internal override PropertyProvider[] BuildProperties()
            {
                var nullInputWireInfo = InputFactory.Property("NullWireInfo", InputPrimitiveType.String);
                return
                [
                    // customized by the NamedSymbol
                    new PropertyProvider($"Int property", MethodSignatureModifiers.Public, typeof(int), "IntProperty", new AutoPropertyBody(true), this, wireInfo: new PropertyWireInformation(SerializationFormat.Default, true, true, true, false, "intProperty", false, false)),
                    // not customized by the NamedSymbol
                    new PropertyProvider($"Spec property", MethodSignatureModifiers.Public, typeof(string), "SpecProperty", new AutoPropertyBody(false), this, wireInfo: new PropertyWireInformation(SerializationFormat.Default, true, true, true, false, "specProperty", false, false)),
                    // customized by the NamedSymbol with null wire info
                    new PropertyProvider($"Null Wire Info property", MethodSignatureModifiers.Public, typeof(string), "NullWireInfoProperty", new AutoPropertyBody(false), this, wireInfo: new PropertyWireInformation(nullInputWireInfo))
                ];
            }

            protected internal override MethodProvider[] BuildMethods()
            {
                var intParam = new ParameterProvider("p", $"I have a wrong name", typeof(int));
                var strParam = new ParameterProvider("strParam", $"I have the correct name", typeof(string));
                var floatParam = new ParameterProvider("floatParam", $"I have the correct name", typeof(float));
                return
                [
                    // customized by the NamedSymbol
                    new MethodProvider(
                        new MethodSignature("Method1", $"I am going to be replaced", MethodSignatureModifiers.Internal | MethodSignatureModifiers.Virtual, typeof(Task<string>), null, [intParam]),
                        Throw(Null),
                        this),
                    // not customized by the NamedSymbol - this is an overload of the above method
                    new MethodProvider(
                        new MethodSignature("Method1", $"I should not be replaced", MethodSignatureModifiers.Public | MethodSignatureModifiers.Virtual, typeof(Task<string>), null, [strParam]),
                        Throw(Null),
                        this),
                    // not customized by the NamedSymbol - this is a new method
                    new MethodProvider(
                        new MethodSignature("Method2", $"I should not be replaced", MethodSignatureModifiers.Public| MethodSignatureModifiers.Virtual, typeof(Task), null, [floatParam]),
                        Throw(Null),
                        this),
                ];
            }

            protected internal override ConstructorProvider[] BuildConstructors()
            {
                var intParam = new ParameterProvider("intParam", $"intParam", typeof(int));
                return
                [
                    // customized by the NamedSymbol
                    new ConstructorProvider(
                        new ConstructorSignature(Type, $"Generated constructor", MethodSignatureModifiers.Internal, [intParam]),
                        Throw(Null),
                        this),
                    // not customized by the NamedSymbol
                    new ConstructorProvider(
                        new ConstructorSignature(Type, $"Not customized constructor", MethodSignatureModifiers.Internal, []),
                        Throw(Null),
                        this),
                ];
            }

            protected internal override FieldProvider[] BuildFields()
            {
                return
                [
                    // customized by the NamedSymbol
                    new FieldProvider(FieldModifiers.Private, typeof(int), "IntField", this, $"Generated IntField"),
                    // not customized by the NamedSymbol
                    new FieldProvider(FieldModifiers.Private, typeof(string), "GeneratedOnlyField", this, $"Generated only field"),
                ];
            }
        }

        private class NoSpecTypeProvider : TypeProvider
        {
            private readonly string _name;
            private readonly string _namespace;
            public NoSpecTypeProvider(string name, string ns)
                : base(InputFactory.Model(name, ns, properties: []))
            {
                _name = name;
                _namespace = ns;
            }

            protected override string BuildRelativeFilePath() => "NamedSymbol";
            protected override string BuildName() => _name;
            protected override string BuildNamespace() => _namespace;

            protected internal override PropertyProvider[] BuildProperties()
            {
                return
                [
                    // customized by the NamedSymbol
                    new PropertyProvider($"Int property", MethodSignatureModifiers.Public, typeof(int), "IntProperty", new AutoPropertyBody(true), this),
                    // not customized by the NamedSymbol
                    new PropertyProvider($"Spec property", MethodSignatureModifiers.Public, typeof(string), "SpecProperty", new AutoPropertyBody(false), this),
                ];
            }
        }
    }
}
