// ------------------------------------------------------------
//  Copyright (c) Microsoft Corporation.  All rights reserved.
//  Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// ------------------------------------------------------------

using System;
using System.Linq;
using Microsoft.OData.Edm;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.OData.Edm;
using Microsoft.OpenApi.OData.Generator;
using Xunit;
using Xunit.Abstractions;

namespace Microsoft.OpenApi.OData.Tests
{
    public class OpenApiSchemaGeneratorTest
    {
        private ITestOutputHelper _output;
        public OpenApiSchemaGeneratorTest(ITestOutputHelper output)
        {
            _output = output;
        }

        [Fact]
        public void CreateSchemasThrowArgumentNullContext()
        {
            // Arrange
            ODataContext context = null;

            // Act & Assert
            Assert.Throws<ArgumentNullException>("context", () => context.CreateSchemas());
        }

        #region StructuredTypeSchema
        [Fact]
        public void CreateStructuredTypeSchemaThrowArgumentNullContext()
        {
            // Arrange
            ODataContext context = null;

            // Act & Assert
            Assert.Throws<ArgumentNullException>("context", () => context.CreateStructuredTypeSchema(structuredType: null));
        }

        [Fact]
        public void CreateStructuredTypeSchemaThrowArgumentNullEnumType()
        {
            // Arrange
            ODataContext context = new ODataContext(EdmCoreModel.Instance);

            // Act & Assert
            Assert.Throws<ArgumentNullException>("structuredType", () => context.CreateStructuredTypeSchema(structuredType: null));
        }

        [Fact]
        public void CreateComplexTypeWithoutBaseSchemaReturnCorrectSchema()
        {
            // Arrange
            IEdmModel model = EdmModelHelper.MultipleInheritanceEdmModel;
            ODataContext context = new ODataContext(model);
            IEdmComplexType complex = model.SchemaElements.OfType<IEdmComplexType>().First(t => t.Name == "Address");
            Assert.NotNull(complex); // Guard

            // Act
            var schema = context.CreateStructuredTypeSchema(complex);

            // Assert
            Assert.NotNull(schema);
            Assert.Equal("object", schema.Type);
            Assert.Null(schema.AllOf);

            Assert.NotNull(schema.Properties);
            Assert.Equal(2, schema.Properties.Count);
            Assert.Equal(new string[] { "Street", "City" }, schema.Properties.Select(e => e.Key));
            Assert.Equal("Complex type 'Address' description.", schema.Description);
            Assert.Equal("Address", schema.Title);

            // Act
            string json = schema.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0);

            // Assert
            Assert.NotNull(json);
            Assert.Equal(@"{
  ""title"": ""Address"",
  ""type"": ""object"",
  ""properties"": {
    ""Street"": {
      ""type"": ""string"",
      ""nullable"": true
    },
    ""City"": {
      ""type"": ""string"",
      ""nullable"": true
    }
  },
  ""description"": ""Complex type 'Address' description."",
  ""example"": {
    ""Street"": ""string"",
    ""City"": ""string""
  }
}".ChangeLineBreaks(), json);
        }

        [Fact]
        public void CreateComplexTypeWithBaseSchemaReturnCorrectSchema()
        {
            // Arrange
            IEdmModel model = EdmModelHelper.MultipleInheritanceEdmModel;
            ODataContext context = new ODataContext(model, new OpenApiConvertSettings
            {
                IEEE754Compatible = true
            });
            IEdmComplexType complex = model.SchemaElements.OfType<IEdmComplexType>().First(t => t.Name == "Tree");
            Assert.NotNull(complex); // Guard

            // Act
            var schema = context.CreateStructuredTypeSchema(complex);

            // Assert
            Assert.NotNull(schema);
            Assert.True(String.IsNullOrEmpty(schema.Type));

            Assert.NotNull(schema.AllOf);
            Assert.Null(schema.AnyOf);
            Assert.Null(schema.OneOf);
            Assert.Null(schema.Properties);

            Assert.Equal(2, schema.AllOf.Count);
            var baseSchema = schema.AllOf.First();
            Assert.NotNull(baseSchema.Reference);
            Assert.Equal(ReferenceType.Schema, baseSchema.Reference.Type);
            Assert.Equal("NS.LandPlant", baseSchema.Reference.Id);

            var declaredSchema = schema.AllOf.Last();
            Assert.Equal("object", declaredSchema.Type);
            Assert.Null(declaredSchema.AllOf);
            Assert.Null(declaredSchema.AnyOf);
            Assert.Null(declaredSchema.OneOf);

            Assert.NotNull(declaredSchema.Properties);
            Assert.Equal(1, declaredSchema.Properties.Count);
            var property = Assert.Single(declaredSchema.Properties);
            Assert.Equal("Price", property.Key);
            Assert.Equal("decimal", property.Value.Format);
            Assert.NotNull(property.Value.AnyOf);
            Assert.Equal(new string[] { "number", "string" }, property.Value.AnyOf.Select(e => e.Type));

            Assert.Equal("Complex type 'Tree' description.", declaredSchema.Description);
            Assert.Equal("Tree", declaredSchema.Title);

            // Act
            string json = schema.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0);

            // Assert
            Assert.NotNull(json);
            Assert.Equal(@"{
  ""allOf"": [
    {
      ""$ref"": ""#/components/schemas/NS.LandPlant""
    },
    {
      ""title"": ""Tree"",
      ""type"": ""object"",
      ""properties"": {
        ""Price"": {
          ""multipleOf"": 1,
          ""anyOf"": [
            {
              ""type"": ""number""
            },
            {
              ""type"": ""string""
            }
          ],
          ""format"": ""decimal""
        }
      },
      ""description"": ""Complex type 'Tree' description.""
    }
  ],
  ""example"": {
    ""Color"": {
      ""@odata.type"": ""NS.Color""
    },
    ""Continent"": {
      ""@odata.type"": ""NS.Continent""
    },
    ""Name"": ""string"",
    ""Price"": ""decimal""
  }
}"
.ChangeLineBreaks(), json);
        }

        [Fact]
        public void CreateEntityTypeWithoutBaseSchemaReturnCorrectSchema()
        {
            // Arrange
            IEdmModel model = EdmModelHelper.MultipleInheritanceEdmModel;
            ODataContext context = new ODataContext(model);
            IEdmEntityType entity = model.SchemaElements.OfType<IEdmEntityType>().First(t => t.Name == "Zoo");
            Assert.NotNull(entity); // Guard

            // Act
            var schema = context.CreateStructuredTypeSchema(entity);

            // Assert
            Assert.NotNull(schema);
            Assert.Equal("object", schema.Type);
            Assert.Null(schema.AllOf);

            Assert.NotNull(schema.Properties);
            Assert.Equal(2, schema.Properties.Count);
            Assert.Equal(new string[] { "Id", "Creatures" }, schema.Properties.Select(e => e.Key));
            Assert.Equal("Entity type 'Zoo' description.", schema.Description);
            Assert.Equal("Zoo", schema.Title);

            // Act
            string json = schema.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0);

            // Assert
            Assert.NotNull(json);
            Assert.Equal(@"{
  ""title"": ""Zoo"",
  ""type"": ""object"",
  ""properties"": {
    ""Id"": {
      ""maximum"": 2147483647,
      ""minimum"": -2147483648,
      ""type"": ""integer"",
      ""format"": ""int32""
    },
    ""Creatures"": {
      ""type"": ""array"",
      ""items"": {
        ""$ref"": ""#/components/schemas/NS.Creature""
      }
    }
  },
  ""description"": ""Entity type 'Zoo' description."",
  ""example"": {
    ""Id"": ""integer (identifier)"",
    ""Creatures"": [
      {
        ""@odata.type"": ""NS.Creature""
      }
    ]
  }
}".ChangeLineBreaks(), json);
        }

        [Fact]
        public void CreateEntityTypeWithBaseSchemaReturnCorrectSchema()
        {
            // Arrange
            IEdmModel model = EdmModelHelper.MultipleInheritanceEdmModel;
            ODataContext context = new ODataContext(model);
            IEdmEntityType entity = model.SchemaElements.OfType<IEdmEntityType>().First(t => t.Name == "Human");
            Assert.NotNull(entity); // Guard

            // Act
            var schema = context.CreateStructuredTypeSchema(entity);

            // Assert
            Assert.NotNull(schema);
            Assert.True(String.IsNullOrEmpty(schema.Type));

            Assert.NotNull(schema.AllOf);
            Assert.Null(schema.AnyOf);
            Assert.Null(schema.OneOf);
            Assert.Null(schema.Properties);

            Assert.Equal(2, schema.AllOf.Count);
            var baseSchema = schema.AllOf.First();
            Assert.NotNull(baseSchema.Reference);
            Assert.Equal(ReferenceType.Schema, baseSchema.Reference.Type);
            Assert.Equal("NS.Animal", baseSchema.Reference.Id);

            var declaredSchema = schema.AllOf.Last();
            Assert.Equal("object", declaredSchema.Type);
            Assert.Null(declaredSchema.AllOf);
            Assert.Null(declaredSchema.AnyOf);
            Assert.Null(declaredSchema.OneOf);

            Assert.NotNull(declaredSchema.Properties);
            Assert.Equal(1, declaredSchema.Properties.Count);
            var property = Assert.Single(declaredSchema.Properties);
            Assert.Equal("Name", property.Key);
            Assert.Equal("string", property.Value.Type);
            Assert.Null(property.Value.OneOf);

            Assert.Equal("Entity type 'Human' description.", declaredSchema.Description);
            Assert.Equal("Human", declaredSchema.Title);

            // Act
            string json = schema.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0);
            _output.WriteLine(json);
            // Assert
            Assert.NotNull(json);
            Assert.Equal(@"{
  ""allOf"": [
    {
      ""$ref"": ""#/components/schemas/NS.Animal""
    },
    {
      ""title"": ""Human"",
      ""type"": ""object"",
      ""properties"": {
        ""Name"": {
          ""type"": ""string""
        }
      },
      ""description"": ""Entity type 'Human' description.""
    }
  ],
  ""example"": {
    ""Id"": ""integer (identifier)"",
    ""Age"": ""integer"",
    ""Name"": ""string""
  }
}"
.ChangeLineBreaks(), json);
        }
        #endregion

        #region EnumTypeSchema
        [Fact]
        public void CreateEnumTypeSchemaThrowArgumentNullContext()
        {
            // Arrange
            ODataContext context = null;

            // Act & Assert
            Assert.Throws<ArgumentNullException>("context", () => context.CreateEnumTypeSchema(enumType: null));
        }

        [Fact]
        public void CreateEnumTypeSchemaThrowArgumentNullEnumType()
        {
            // Arrange
            ODataContext context = new ODataContext(EdmCoreModel.Instance);

            // Act & Assert
            Assert.Throws<ArgumentNullException>("enumType", () => context.CreateEnumTypeSchema(enumType: null));
        }

        [Fact]
        public void CreateEnumTypeSchemaReturnCorrectSchema()
        {
            // Arrange
            IEdmModel model = EdmModelHelper.BasicEdmModel;
            ODataContext context = new ODataContext(model);
            IEdmEnumType enumType = model.SchemaElements.OfType<IEdmEnumType>().First(t => t.Name == "Color");
            Assert.NotNull(enumType); // Guard

            // Act
            var schema = context.CreateEnumTypeSchema(enumType);

            // Assert
            Assert.NotNull(schema);
            Assert.Equal("string", schema.Type);
            Assert.Equal("Enum type 'Color' description.", schema.Description);
            Assert.Equal("Color", schema.Title);

            Assert.NotNull(schema.Enum);
            Assert.Equal(2, schema.Enum.Count);
            Assert.Equal(new string[] { "Blue", "White" }, schema.Enum.Select(e => ((OpenApiString)e).Value));

            // Act
            string json = schema.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0);

            // Assert
            Assert.NotNull(json);
            Assert.Equal(@"{
  ""title"": ""Color"",
  ""enum"": [
    ""Blue"",
    ""White""
  ],
  ""type"": ""string"",
  ""description"": ""Enum type 'Color' description.""
}".ChangeLineBreaks(), json);
        }
        #endregion

        #region EdmPropertySchema
        [Theory]
        [InlineData(OpenApiSpecVersion.OpenApi2_0)]
        [InlineData(OpenApiSpecVersion.OpenApi3_0)]
        public void CreatePropertySchemaForNonNullableEnumPropertyReturnSchema(OpenApiSpecVersion specVersion)
        {
            // Arrange
            IEdmModel model = EdmModelHelper.BasicEdmModel;
            ODataContext context = new ODataContext(model);

            context.Settings.OpenApiSpecVersion = specVersion;

            IEdmEnumType enumType = model.SchemaElements.OfType<IEdmEnumType>().First(e => e.Name == "Color");
            EdmEntityType entitType = new EdmEntityType("NS", "Entity");
            IEdmProperty property = new EdmStructuralProperty(entitType, "ColorEnumValue", new EdmEnumTypeReference(enumType, false), "yellow");

            // Act
            var schema = context.CreatePropertySchema(property);
            Assert.NotNull(schema);
            string json = schema.SerializeAsJson(specVersion);

            // Assert

            if (specVersion == OpenApiSpecVersion.OpenApi2_0)
            {
                Assert.Equal(@"{
  ""$ref"": ""#/definitions/DefaultNs.Color""
}".ChangeLineBreaks(), json);
            }
            else
            {
                Assert.Equal(@"{
  ""anyOf"": [
    {
      ""$ref"": ""#/components/schemas/DefaultNs.Color""
    }
  ],
  ""default"": ""yellow""
}".ChangeLineBreaks(), json);
            }
        }

        [Theory]
        [InlineData(OpenApiSpecVersion.OpenApi3_0)]
        [InlineData(OpenApiSpecVersion.OpenApi2_0)]
        public void CreatePropertySchemaForNullableEnumPropertyReturnSchema(OpenApiSpecVersion specVersion)
        {
            // Arrange
            IEdmModel model = EdmModelHelper.BasicEdmModel;
            ODataContext context = new ODataContext(model);

            context.Settings.OpenApiSpecVersion = specVersion;

            IEdmEnumType enumType = model.SchemaElements.OfType<IEdmEnumType>().First(e => e.Name == "Color");
            EdmEntityType entitType = new EdmEntityType("NS", "Entity");
            IEdmProperty property = new EdmStructuralProperty(entitType, "ColorEnumValue", new EdmEnumTypeReference(enumType, true), "yellow");

            // Act
            var schema = context.CreatePropertySchema(property);
            Assert.NotNull(schema);
            string json = schema.SerializeAsJson(specVersion);
            _output.WriteLine(json);
            
            // Assert
            if (specVersion == OpenApiSpecVersion.OpenApi2_0)
            {
                Assert.Equal(@"{
  ""$ref"": ""#/definitions/DefaultNs.Color""
}".ChangeLineBreaks(), json);
            }
            else
            {
                Assert.Equal(@"{
  ""anyOf"": [
    {
      ""$ref"": ""#/components/schemas/DefaultNs.Color""
    }
  ],
  ""default"": ""yellow"",
  ""nullable"": true
}".ChangeLineBreaks(), json);
            }
        }
        #endregion

        [Fact]
        public void NonNullableBooleanPropertyWithDefaultValueWorks()
        {
            // Arrange
            ODataContext context = new ODataContext(EdmModelHelper.BasicEdmModel);
            EdmEntityType entitType = new EdmEntityType("NS", "Entity");
            IEdmStructuralProperty property = new EdmStructuralProperty(
                entitType, "BooleanValue", EdmCoreModel.Instance.GetBoolean(false), "false");

            // Act
            var schema = context.CreatePropertySchema(property);

            // Assert
            Assert.NotNull(schema);
            Assert.Equal("boolean", schema.Type);

            string json = schema.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0);
            Assert.Equal(@"{
  ""type"": ""boolean"",
  ""default"": false
}".ChangeLineBreaks(), json);
        }

        [Fact]
        public void NonNullableBinaryPropertyWithBothMaxLengthAndDefaultValueWorks()
        {
            // Arrange
            ODataContext context = new ODataContext(EdmModelHelper.BasicEdmModel);
            EdmEntityType entitType = new EdmEntityType("NS", "Entity");
            var binaryType = new EdmBinaryTypeReference(EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Binary),
                false, false, 44);
            IEdmStructuralProperty property = new EdmStructuralProperty(
                entitType, "BinaryValue", binaryType, "T0RhdGE");

            // Act
            var schema = context.CreatePropertySchema(property);

            // Assert
            Assert.NotNull(schema);
            Assert.Equal("string", schema.Type);

            string json = schema.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0);
            Assert.Equal(@"{
  ""maxLength"": 44,
  ""type"": ""string"",
  ""format"": ""base64url"",
  ""default"": ""T0RhdGE""
}".ChangeLineBreaks(), json);
        }

        [Fact]
        public void NonNullableIntegerPropertyWithDefaultValueWorks()
        {
            // Arrange
            ODataContext context = new ODataContext(EdmModelHelper.BasicEdmModel);
            EdmEntityType entitType = new EdmEntityType("NS", "Entity");
            IEdmStructuralProperty property = new EdmStructuralProperty(
                entitType, "IntegerValue", EdmCoreModel.Instance.GetInt32(false), "-128");

            // Act
            var schema = context.CreatePropertySchema(property);

            // Assert
            Assert.NotNull(schema);
            Assert.Equal("integer", schema.Type);

            string json = schema.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0);
            Assert.Equal(@"{
  ""maximum"": 2147483647,
  ""minimum"": -2147483648,
  ""type"": ""integer"",
  ""format"": ""int32"",
  ""default"": -128
}".ChangeLineBreaks(), json);
        }

        [Fact]
        public void NonNullableDoublePropertyWithDefaultStringWorks()
        {
            // Arrange
            ODataContext context = new ODataContext(EdmModelHelper.BasicEdmModel);
            EdmEntityType entitType = new EdmEntityType("NS", "Entity");
            IEdmStructuralProperty property = new EdmStructuralProperty(
                entitType, "DoubleValue", EdmCoreModel.Instance.GetDouble(false), "3.1415926535897931");

            // Act
            var schema = context.CreatePropertySchema(property);

            // Assert
            Assert.NotNull(schema);
            Assert.Null(schema.Type);

            string json = schema.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0);

            Assert.Equal(@"{
  ""anyOf"": [
    {
      ""type"": ""number""
    },
    {
      ""type"": ""string""
    },
    {
      ""enum"": [
        ""-INF"",
        ""INF"",
        ""NaN""
      ]
    }
  ],
  ""format"": ""double"",
  ""default"": ""3.1415926535897931""
}".ChangeLineBreaks(), json);
        }
    }
}
