/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using NUnit.Framework;
using Avro.Specific;

namespace Avro.Test.AvroGen
{
    [TestFixture]

    class AvroGenSchemaTests
    {
        private const string _customConversionWithLogicalTypes = @"
{
  ""namespace"": ""org.apache.avro.codegentest.testdata"",
  ""type"": ""record"",
  ""name"": ""CustomConversionWithLogicalTypes"",
  ""doc"" : ""Test custom conversion and logical types in generated Java classes"",
  ""fields"": [
    {
      ""name"": ""customEnum"",
      ""type"": [""null"", {
        ""namespace"": ""org.apache.avro.codegentest.testdata"",
        ""name"": ""CustomAvroEnum"",
        ""type"": ""enum"",
        ""logicalType"": ""custom-enum"",
        ""symbols"": [""ONE"", ""TWO"", ""THREE""]
    }]
    }]
}
";

        private const string _logicalTypesWithCustomConversion = @"
{
""namespace"": ""org.apache.avro.codegentest.testdata"",
  ""type"": ""record"",
  ""name"": ""LogicalTypesWithCustomConversion"",
  ""doc"" : ""Test unions with logical types in generated Java classes"",
  ""fields"": [
    {""name"": ""nullableCustomField"",  ""type"": [""null"", {""type"": ""bytes"", ""logicalType"": ""decimal"", ""precision"": 9, ""scale"": 2}], ""default"": null},
    { ""name"": ""nonNullCustomField"",  ""type"": { ""type"": ""bytes"", ""logicalType"": ""decimal"", ""precision"": 9, ""scale"": 2} },
    { ""name"": ""nullableFixedSizeString"",  ""type"": [""null"", { ""type"": ""bytes"", ""logicalType"": ""fixed-size-string"", ""minLength"": 1, ""maxLength"": 50}], ""default"": null},
    { ""name"": ""nonNullFixedSizeString"",  ""type"": { ""type"": ""bytes"", ""logicalType"": ""fixed-size-string"", ""minLength"": 1, ""maxLength"": 50} }
  ]
}
";

        private const string _logicalTypesWithDefaults = @"
{
""namespace"": ""org.apache.avro.codegentest.testdata"",
  ""type"": ""record"",
  ""name"": ""LogicalTypesWithDefaults"",
  ""doc"" : ""Test logical types and default values in generated Java classes"",
  ""fields"": [
    {""name"": ""nullableDate"",  ""type"": [{""type"": ""int"", ""logicalType"": ""date""}, ""null""], ""default"": 1234},
    { ""name"": ""nonNullDate"",  ""type"": { ""type"": ""int"", ""logicalType"": ""date""}, ""default"": 1234}
  ]
}";

        private const string _nestedLogicalTypesArray = @"
{""namespace"": ""org.apache.avro.codegentest.testdata"",
  ""type"": ""record"",
  ""name"": ""NestedLogicalTypesArray"",
  ""doc"" : ""Test nested types with logical types in generated Java classes"",
  ""fields"": [
    {
      ""name"": ""arrayOfRecords"",
      ""type"": {
        ""type"": ""array"",
        ""items"": {
          ""namespace"": ""org.apache.avro.codegentest.testdata"",
          ""name"": ""RecordInArray"",
          ""type"": ""record"",
          ""fields"": [
            {
              ""name"": ""nullableDateField"",
              ""type"": [""null"", {""type"": ""int"", ""logicalType"": ""date""}]
            }
          ]
        }
      }
    }]
}
";

        private const string _nestedLogicalTypesMap = @"
{""namespace"": ""org.apache.avro.codegentest.testdata"",
  ""type"": ""record"",
  ""name"": ""NestedLogicalTypesMap"",
  ""doc"" : ""Test nested types with logical types in generated Java classes"",
  ""fields"": [
    {
      ""name"": ""mapOfRecords"",
      ""type"": {
        ""type"": ""map"",
        ""values"": {
          ""namespace"": ""org.apache.avro.codegentest.testdata"",
          ""name"": ""RecordInMap"",
          ""type"": ""record"",
          ""fields"": [
            {
              ""name"": ""nullableDateField"",
              ""type"": [""null"", {""type"": ""int"", ""logicalType"": ""date""}]
            }
          ]
        }
      }
    }]
}";

        private const string _nestedLogicalTypesRecord = @"
{""namespace"": ""org.apache.avro.codegentest.testdata"",
  ""type"": ""record"",
  ""name"": ""NestedLogicalTypesRecord"",
  ""doc"" : ""Test nested types with logical types in generated Java classes"",
  ""fields"": [
    {
      ""name"": ""nestedRecord"",
      ""type"": {
        ""namespace"": ""org.apache.avro.codegentest.testdata"",
        ""type"": ""record"",
        ""name"": ""NestedRecord"",
        ""fields"": [
          {
            ""name"": ""nullableDateField"",
            ""type"": [""null"", {""type"": ""int"", ""logicalType"": ""date""}]
          }
        ]
      }
    }]
}";

        private const string _nestedLogicalTypesUnionFixedDecimal = @"
{""namespace"": ""org.apache.avro.codegentest.testdata"",
  ""type"": ""record"",
  ""name"": ""NestedLogicalTypesUnionFixedDecimal"",
  ""doc"" : ""Test nested types with logical types in generated Java classes"",
  ""fields"": [
    {
      ""name"": ""unionOfFixedDecimal"",
      ""type"": [""null"", {
        ""namespace"": ""org.apache.avro.codegentest.testdata"",
        ""name"": ""FixedInUnion"",
        ""type"": {
            ""type"": ""fixed"",
            ""size"": 12,
            ""name"": ""FixedName"",
        },
        ""logicalType"": ""decimal"",
        ""precision"": 28,
        ""scale"": 15
      }]
    }]
}";

        private const string _nestedLogicalTypesUnion = @"
{""namespace"": ""org.apache.avro.codegentest.testdata"",
  ""type"": ""record"",
  ""name"": ""NestedLogicalTypesUnion"",
  ""doc"" : ""Test nested types with logical types in generated Java classes"",
  ""fields"": [
    {
      ""name"": ""unionOfRecords"",
      ""type"": [""null"", {
        ""namespace"": ""org.apache.avro.codegentest.testdata"",
        ""name"": ""RecordInUnion"",
        ""type"": ""record"",
        ""fields"": [
          {
            ""name"": ""nullableDateField"",
            ""type"": [""null"", {""type"": ""int"", ""logicalType"": ""date""}]
          }
        ]
      }]
    }]
}";

        private const string _nestedSomeNamespaceRecord = @"
{""namespace"": ""org.apache.avro.codegentest.some"",
  ""type"": ""record"",
  ""name"": ""NestedSomeNamespaceRecord"",
  ""doc"" : ""Test nested types with different namespace than the outer type"",
  ""fields"": [
    {
      ""name"": ""nestedRecord"",
      ""type"": {
        ""namespace"": ""org.apache.avro.codegentest.other"",
        ""type"": ""record"",
        ""name"": ""NestedOtherNamespaceRecord"",
        ""fields"": [
          {
            ""name"": ""someField"",
            ""type"": ""int""
          }
        ]
      }
    }]
}";

        private const string _nullableLogicalTypesArray = @"
{""namespace"": ""org.apache.avro.codegentest.testdata"",
  ""type"": ""record"",
  ""name"": ""NullableLogicalTypesArray"",
  ""doc"" : ""Test nested types with logical types in generated Java classes"",
  ""fields"": [
    {
      ""name"": ""arrayOfLogicalType"",
      ""type"": {
        ""type"": ""array"",
        ""items"": [""null"", {""type"": ""int"", ""logicalType"": ""date""}]
      }
    }]
}";

        private const string _nullableLogicalTypes = @"
{""namespace"": ""org.apache.avro.codegentest.testdata"",
  ""type"": ""record"",
  ""name"": ""NullableLogicalTypes"",
  ""doc"" : ""Test unions with logical types in generated Java classes"",
  ""fields"": [
    {""name"": ""nullableDate"",  ""type"": [""null"", {""type"": ""int"", ""logicalType"": ""date""}], ""default"": null}
  ]
}";

        private const string _stringLogicalType = @"
{
  ""namespace"": ""org.apache.avro.codegentest.testdata"",
  ""type"": ""record"",
  ""name"": ""StringLogicalType"",
  ""doc"": ""Test logical type applied to field of type string"",
  ""fields"": [
    {
      ""name"": ""someIdentifier"",
      ""type"": {
        ""type"": ""string"",
        ""logicalType"": ""uuid""
      }
},
    {
    ""name"": ""someJavaString"",
      ""type"": ""string"",
      ""doc"": ""Just to ensure no one removed <stringType>String</stringType> because this is the basis of this test""
    }
  ]
}";

        // https://issues.apache.org/jira/browse/AVRO-2883
        private const string _schema_avro_2883 = @"
{
  ""type"" : ""record"",
  ""name"" : ""TestModel"",
  ""namespace"" : ""my.avro.ns"",
  ""fields"" : [ {
    ""name"" : ""eventType"",
    ""type"" : {
      ""type"" : ""enum"",
      ""name"" : ""EventType"",
      ""symbols"" : [ ""CREATE"", ""UPDATE"", ""DELETE"" ]
    }
} ]
}";

        // https://issues.apache.org/jira/browse/AVRO-3046
        private const string _schema_avro_3046 = @"
{
  ""type"": ""record"",
  ""name"": ""ExampleRecord"",
  ""namespace"": ""com.example"",
  ""fields"": [
    {
      ""name"": ""Id"",
      ""type"": ""string"",
      ""logicalType"": ""UUID""
    },
    {
      ""name"": ""InnerRecord"",
      ""type"": {
        ""type"": ""record"",
        ""name"": ""InnerRecord"",
        ""fields"": [
          {
            ""name"": ""Id"",
            ""type"": ""string"",
            ""logicalType"": ""UUID""
          }
        ]
      }
    }
  ]
}";

        private Assembly TestSchema(
            string schema,
            IEnumerable<string> typeNamesToCheck = null,
            IEnumerable<KeyValuePair<string, string>> namespaceMapping = null,
            IEnumerable<string> generatedFilesToCheck = null)
        {
            // Create temp folder
            string outputDir = AvroGenHelper.CreateEmptyTemporaryFolder(out string uniqueId);

            try
            {
                // Save schema
                string schemaFileName = Path.Combine(outputDir, $"{uniqueId}.avsc");
                System.IO.File.WriteAllText(schemaFileName, schema);

                // Generate from schema file
                Assert.That(AvroGenTool.GenSchema(schemaFileName, outputDir, namespaceMapping ?? new Dictionary<string, string>(), false), Is.EqualTo(0));

                // Check if all generated files exist
                if (generatedFilesToCheck != null)
                {
                    foreach (string generatedFile in generatedFilesToCheck)
                    {
                        Assert.That(new FileInfo(Path.Combine(outputDir, generatedFile)), Does.Exist);
                    }
                }

                // Compile into netstandard library and load assembly
                Assembly assembly = AvroGenHelper.CompileCSharpFilesIntoLibrary(
                    new DirectoryInfo(outputDir)
                        .EnumerateFiles("*.cs", SearchOption.AllDirectories)
                        .Select(fi => fi.FullName),
                        uniqueId);

                if (typeNamesToCheck != null)
                {
                    // Check if the compiled code has the same number of types defined as the check list
                    Assert.That(typeNamesToCheck.Count(), Is.EqualTo(assembly.DefinedTypes.Count()));

                    // Check if types available in compiled assembly
                    foreach (string typeName in typeNamesToCheck)
                    {
                        Type type = assembly.GetType(typeName);
                        Assert.That(type, Is.Not.Null);

                        // Instantiate
                        object obj = Activator.CreateInstance(type);
                        Assert.That(obj, Is.Not.Null);
                    }
                }

                // Verify GeneratedCodeAttribute
                foreach(System.Reflection.TypeInfo definedType in assembly.DefinedTypes)
                {
                    var generatedAttributes = definedType.CustomAttributes.Where(x => x.AttributeType.FullName == "System.CodeDom.Compiler.GeneratedCodeAttribute");
                    Assert.That(generatedAttributes, Is.Not.Null);
                }

                return assembly;
            }
            finally
            {
                Directory.Delete(outputDir, true);
            }
        }

        [TestCase(
            _logicalTypesWithDefaults,
            new string[]
            {
                "org.apache.avro.codegentest.testdata.LogicalTypesWithDefaults"
            },
            new string[]
            {
                "org/apache/avro/codegentest/testdata/LogicalTypesWithDefaults.cs"
            })]
        [TestCase(
            _nestedLogicalTypesArray,
            new string[]
            {
                "org.apache.avro.codegentest.testdata.NestedLogicalTypesArray",
                "org.apache.avro.codegentest.testdata.RecordInArray"
            },
            new string[]
            {
                "org/apache/avro/codegentest/testdata/NestedLogicalTypesArray.cs",
                "org/apache/avro/codegentest/testdata/RecordInArray.cs"
            })]
        [TestCase(
            _nestedLogicalTypesMap,
            new string[]
            {
                "org.apache.avro.codegentest.testdata.NestedLogicalTypesMap",
                "org.apache.avro.codegentest.testdata.RecordInMap"
            },
            new string[]
            {
                "org/apache/avro/codegentest/testdata/NestedLogicalTypesMap.cs",
                "org/apache/avro/codegentest/testdata/RecordInMap.cs"
            })]
        [TestCase(
            _nestedLogicalTypesRecord,
            new string[]
            {
                "org.apache.avro.codegentest.testdata.NestedLogicalTypesRecord",
                "org.apache.avro.codegentest.testdata.NestedRecord"
            },
            new string[]
            {
                "org/apache/avro/codegentest/testdata/NestedLogicalTypesRecord.cs",
                "org/apache/avro/codegentest/testdata/NestedRecord.cs"
            })]
        [TestCase(
            _nestedLogicalTypesUnion,
            new string[]
            {
                "org.apache.avro.codegentest.testdata.NestedLogicalTypesUnion",
                "org.apache.avro.codegentest.testdata.RecordInUnion"
            },
            new string[]
            {
                "org/apache/avro/codegentest/testdata/NestedLogicalTypesUnion.cs",
                "org/apache/avro/codegentest/testdata/RecordInUnion.cs"
            })]
        [TestCase(
            _nestedSomeNamespaceRecord,
            new string[]
            {
                "org.apache.avro.codegentest.some.NestedSomeNamespaceRecord",
                "org.apache.avro.codegentest.other.NestedOtherNamespaceRecord"
            },
            new string[]
            {
                "org/apache/avro/codegentest/some/NestedSomeNamespaceRecord.cs",
                "org/apache/avro/codegentest/other/NestedOtherNamespaceRecord.cs"
            })]
        [TestCase(
            _nestedLogicalTypesUnionFixedDecimal,
            new string[]
            {
                "org.apache.avro.codegentest.testdata.NestedLogicalTypesUnionFixedDecimal"
            },
            new string[]
            {
                "org/apache/avro/codegentest/testdata/NestedLogicalTypesUnionFixedDecimal.cs"
            })]
        [TestCase(
            _nullableLogicalTypes,
            new string[]
            {
               "org.apache.avro.codegentest.testdata.NullableLogicalTypes"
            },
            new string[]
            {
               "org/apache/avro/codegentest/testdata/NullableLogicalTypes.cs"
            })]
        [TestCase(
            _nullableLogicalTypesArray,
            new string[]
            {
                "org.apache.avro.codegentest.testdata.NullableLogicalTypesArray"
            },
            new string[]
            {
                "org/apache/avro/codegentest/testdata/NullableLogicalTypesArray.cs"
            })]
        [TestCase(
            _schema_avro_2883,
            new string[]
            {
                "my.avro.ns.TestModel",
                "my.avro.ns.EventType",
            },
            new string[]
            {
                "my/avro/ns/TestModel.cs",
                "my/avro/ns/EventType.cs"
            })]
        public void GenerateSchema(string schema, IEnumerable<string> typeNamesToCheck, IEnumerable<string> generatedFilesToCheck)
        {
            AvroGenHelper.TestSchema(schema, typeNamesToCheck, generatedFilesToCheck: generatedFilesToCheck);
        }

        [TestCase(
            _nullableLogicalTypesArray,
            "org.apache.avro.codegentest.testdata", "org.apache.csharp.codegentest.testdata",
            new string[]
            {
                "org.apache.csharp.codegentest.testdata.NullableLogicalTypesArray"
            },
            new string[]
            {
                "org/apache/csharp/codegentest/testdata/NullableLogicalTypesArray.cs"
            })]
        [TestCase(
            _nestedLogicalTypesUnion,
            "org.apache.avro.codegentest.testdata", "org.apache.csharp.codegentest.testdata",
            new string[]
            {
                "org.apache.csharp.codegentest.testdata.NestedLogicalTypesUnion",
                "org.apache.csharp.codegentest.testdata.RecordInUnion"
            },
            new string[]
            {
                "org/apache/csharp/codegentest/testdata/NestedLogicalTypesUnion.cs",
                "org/apache/csharp/codegentest/testdata/RecordInUnion.cs"
            })]
        [TestCase(
            _schema_avro_2883,
            "my.avro.ns", "my.csharp.ns",
            new string[]
            {
                "my.csharp.ns.TestModel",
                "my.csharp.ns.EventType",
            },
            new string[]
            {
                "my/csharp/ns/TestModel.cs",
                "my/csharp/ns/EventType.cs"
            })]
        [TestCase(
            _schema_avro_3046,
            "com.example", "Example",
            new string[]
            {
                "Example.ExampleRecord",
                "Example.InnerRecord",
            },
            new string[]
            {
                "Example/ExampleRecord.cs",
                "Example/InnerRecord.cs"
            })]
        [TestCase(
            _nullableLogicalTypesArray,
            "org.apache.avro.codegentest.testdata", "org.apache.@return.@int", // Reserved keywords in namespace
            new string[]
            {
                "org.apache.return.int.NullableLogicalTypesArray"
            },
            new string[]
            {
                "org/apache/return/int/NullableLogicalTypesArray.cs"
            })]
        [TestCase(
            _nullableLogicalTypesArray,
            "org.apache.avro.codegentest.testdata", "org.apache.value.partial", // Contextual keywords in namespace
            new string[]
            {
                "org.apache.value.partial.NullableLogicalTypesArray"
            },
            new string[]
            {
                "org/apache/value/partial/NullableLogicalTypesArray.cs"
            })]
        [TestCase(@"
{
    ""type"": ""fixed"",
    ""namespace"": ""com.base"",
    ""name"": ""MD5"",
    ""size"": 16
}",
            "com.base", "SchemaTest",
            new string[]
            {
                "SchemaTest.MD5"
            },
            new string[]
            {
                "SchemaTest/MD5.cs"
            })]
        [TestCase(@"
{
    ""type"": ""fixed"",
    ""namespace"": ""com.base"",
    ""name"": ""MD5"",
    ""size"": 16
}",
            "miss", "SchemaTest",
            new string[]
            {
                "com.base.MD5"
            },
            new string[]
            {
                "com/base/MD5.cs"
            })]
        public void GenerateSchemaWithNamespaceMapping(
            string schema,
            string namespaceMappingFrom,
            string namespaceMappingTo,
            IEnumerable<string> typeNamesToCheck,
            IEnumerable<string> generatedFilesToCheck)
        {
            AvroGenHelper.TestSchema(schema, typeNamesToCheck, new Dictionary<string, string> { { namespaceMappingFrom, namespaceMappingTo } }, generatedFilesToCheck);
        }

        [TestCase(_logicalTypesWithCustomConversion)]
        [TestCase(_customConversionWithLogicalTypes)]
        public void UnknownLogicalTypesFallbackToBaseType(string schema)
        {
            // Create temp folder
            string outputDir = AvroGenHelper.CreateEmptyTemporaryFolder(out string uniqueId);

            try
            {
                // Save schema
                string schemaFileName = Path.Combine(outputDir, $"{uniqueId}.avsc");
                System.IO.File.WriteAllText(schemaFileName, schema);

                // Assert that the generator successfully runs (exit code 0)
                // by ignoring the unknown logical types and using the underlying base types
                Assert.That(AvroGenTool.GenSchema(schemaFileName, outputDir, new Dictionary<string, string>(), false), Is.EqualTo(0));
            }
            finally
            {
                Directory.Delete(outputDir, true);
            }
        }

        [TestCase(@"
{
    ""type"" : ""record"",
    ""name"" : ""ClassKeywords"",
    ""namespace"" : ""com.base"",
    ""fields"" :
        [
            { ""name"" : ""int"", ""type"" : ""int"" },
            { ""name"" : ""base"", ""type"" : ""long"" },
            { ""name"" : ""event"", ""type"" : ""boolean"" },
            { ""name"" : ""foreach"", ""type"" : ""double"" },
            { ""name"" : ""bool"", ""type"" : ""float"" },
            { ""name"" : ""internal"", ""type"" : ""bytes"" },
            { ""name"" : ""while"", ""type"" : ""string"" },
            { ""name"" : ""return"", ""type"" : ""null"" },
            { ""name"" : ""enum"", ""type"" : { ""type"" : ""enum"", ""name"" : ""class"", ""symbols"" : [ ""Unknown"", ""A"", ""B"" ], ""default"" : ""Unknown"" } },
            { ""name"" : ""string"", ""type"" : { ""type"": ""fixed"", ""size"": 16, ""name"": ""static"" } }
        ]
}",
            new object[] { "com.base.ClassKeywords", typeof(int), typeof(long), typeof(bool), typeof(double), typeof(float), typeof(byte[]), typeof(string), typeof(object), "com.base.class", "com.base.static" })]
        [TestCase(@"
{
    ""type"" : ""record"",
    ""name"" : ""AvroNamespaceType"",
    ""namespace"" : ""My.Avro"",
    ""fields"" :
        [
            { ""name"" : ""justenum"", ""type"" : { ""type"" : ""enum"", ""name"" : ""justenumEnum"", ""symbols"" : [ ""One"", ""Two"" ] } },
        ]
}",
            new object[] { "My.Avro.AvroNamespaceType", "My.Avro.justenumEnum" })]
        [TestCase(@"
{
    ""type"" : ""record"",
    ""name"" : ""SchemaObject"",
    ""namespace"" : ""schematest"",
    ""fields"" :
        [
            { ""name"" : ""myobject"", ""type"" :
                [
                    ""null"",
                    { ""type"" : ""array"", ""items"" :
                        [
                            ""null"",
                            { ""type"" : ""enum"", ""name"" : ""MyEnum"", ""symbols"" : [ ""A"", ""B"" ] },
                            { ""type"": ""fixed"", ""size"": 16, ""name"": ""MyFixed"" }
                        ]
                    }
                ]
            }
        ]
}",
            new object[] { "schematest.SchemaObject", typeof(IList<object>) })]
        [TestCase(@"
{
  ""type"" : ""record"",
  ""name"" : ""LogicalTypes"",
  ""namespace"" : ""schematest"",
  ""fields"" :
    [
      { ""name"" : ""nullibleguid"", ""type"" : [""null"", {""type"": ""string"", ""logicalType"": ""uuid"" } ]},
      { ""name"" : ""guid"", ""type"" : {""type"": ""string"", ""logicalType"": ""uuid"" } },
      { ""name"" : ""nullibletimestampmillis"", ""type"" : [""null"", {""type"": ""long"", ""logicalType"": ""timestamp-millis""}]  },
      { ""name"" : ""timestampmillis"", ""type"" : {""type"": ""long"", ""logicalType"": ""timestamp-millis""} },
      { ""name"" : ""nullibiletimestampmicros"", ""type"" : [""null"", {""type"": ""long"", ""logicalType"": ""timestamp-micros""}]  },
      { ""name"" : ""timestampmicros"", ""type"" : {""type"": ""long"", ""logicalType"": ""timestamp-micros""} },
      { ""name"" : ""nulliblelocaltimestampmillis"", ""type"" : [""null"", {""type"": ""long"", ""logicalType"": ""local-timestamp-millis""}]  },
      { ""name"" : ""localtimestampmillis"", ""type"" : {""type"": ""long"", ""logicalType"": ""local-timestamp-millis""} },
      { ""name"" : ""nullibilelocaltimestampmicros"", ""type"" : [""null"", {""type"": ""long"", ""logicalType"": ""local-timestamp-micros""}]  },
      { ""name"" : ""locallocaltimestampmicros"", ""type"" : {""type"": ""long"", ""logicalType"": ""local-timestamp-micros""} },
      { ""name"" : ""nullibiletimemicros"", ""type"" : [""null"", {""type"": ""long"", ""logicalType"": ""time-micros""}]  },
      { ""name"" : ""timemicros"", ""type"" : {""type"": ""long"", ""logicalType"": ""time-micros""} },
      { ""name"" : ""nullibiletimemillis"", ""type"" : [""null"", {""type"": ""int"", ""logicalType"": ""time-millis""}]  },
      { ""name"" : ""timemillis"", ""type"" : {""type"": ""int"", ""logicalType"": ""time-millis""} },
      { ""name"" : ""nullibledecimal"", ""type"" : [""null"", {""type"": ""bytes"", ""logicalType"": ""decimal"", ""precision"": 4, ""scale"": 2}]  },
      { ""name"" : ""decimal"", ""type"" : {""type"": ""bytes"", ""logicalType"": ""decimal"", ""precision"": 4, ""scale"": 2} },
      { ""name"" : ""nullibledecimalfixed"", ""type"" : [""null"", {""type"": {""type"" : ""fixed"", ""size"": 16, ""name"": ""ndf""}, ""logicalType"": ""decimal"", ""precision"": 4, ""scale"": 2}]  },
      { ""name"" : ""decimalfixed"", ""type"" : {""type"": {""type"" : ""fixed"", ""size"": 16, ""name"": ""df""}, ""logicalType"": ""decimal"", ""precision"": 4, ""scale"": 2} }
    ]
}",
            new object[] { "schematest.LogicalTypes", typeof(Guid?), typeof(Guid), typeof(DateTime?), typeof(DateTime), typeof(DateTime?), typeof(DateTime), typeof(DateTime?), typeof(DateTime), typeof(DateTime?), typeof(DateTime), typeof(TimeSpan?), typeof(TimeSpan), typeof(TimeSpan?), typeof(TimeSpan), typeof(AvroDecimal?), typeof(AvroDecimal), typeof(AvroDecimal?), typeof(AvroDecimal) })]
        [TestCase(@"
{
  ""namespace"": ""enum.base"",
  ""type"": ""record"",
  ""name"": ""EnumInDifferentNamespace"",
  ""doc"": ""Test enum with a default value in a different namespace"",
  ""fields"": [
    {
      ""name"": ""anEnum"",
      ""type"": {
        ""namespace"": ""enum.base.other"",
        ""type"": ""enum"",
        ""name"": ""AnEnum"",
        ""symbols"": [
          ""A"",
          ""B""
        ],
        ""default"": ""A""
      }
    }
  ]
}",
            new object[] { "enum.base.EnumInDifferentNamespace", "enum.base.other.AnEnum" })]
        public void GenerateSchemaCheckFields(string schema, object[] result)
        {
            Assembly assembly = AvroGenHelper.TestSchema(schema);

            // Instantiate object
            Type type = assembly.GetType((string)result[0]);
            Assert.That(type, Is.Not.Null);

            ISpecificRecord record = Activator.CreateInstance(type) as ISpecificRecord;
            Assert.IsNotNull(record);

            // test type of each fields
            for (int i = 1; i < result.Length; ++i)
            {
                object field = record.Get(i - 1);
                Type stype;
                if (result[i].GetType() == typeof(string))
                {
                    Type t = assembly.GetType((string)result[i]);
                    Assert.That(record, Is.Not.Null);

                    object obj = Activator.CreateInstance(t);
                    Assert.That(obj, Is.Not.Null);
                    stype = obj.GetType();
                }
                else
                {
                    stype = (Type)result[i];
                }
                if (!stype.IsValueType)
                {
                    Assert.That(field, Is.Null);   // can't test reference type, it will be null
                }
                else if (stype.IsValueType && field == null)
                {
                    Assert.That(field, Is.Null); // nullable value type, so we can't get the type using GetType
                }
                else
                {
                    Assert.That(field, Is.Not.Null);
                    Assert.That(field.GetType(), Is.EqualTo(stype));
                }
            }
        }

        [TestCase(
            _nullableLogicalTypesArray,
            new string[]
            {
                "org.apache.avro.codegentest.testdata.NullableLogicalTypesArray"
            },
            new string[]
            {
                "NullableLogicalTypesArray.cs"
            })]
        [TestCase(
            _nestedSomeNamespaceRecord,
            new string[]
            {
                "org.apache.avro.codegentest.some.NestedSomeNamespaceRecord",
                "org.apache.avro.codegentest.other.NestedOtherNamespaceRecord"
            },
            new string[]
            {
                "NestedSomeNamespaceRecord.cs",
                "NestedOtherNamespaceRecord.cs"
            })]
        [TestCase(_schema_avro_2883,
            new string[]
            {
                "my.avro.ns.TestModel",
                "my.avro.ns.EventType",
            },
            new string[]
            {
                "TestModel.cs",
                "EventType.cs"
            })]
        public void GenerateSchemaWithSkipDirectoriesOption(string schema, IEnumerable<string> typeNamesToCheck, IEnumerable<string> generatedFilesToCheck)
        {
            AvroGenHelper.TestSchema(schema, typeNamesToCheck, generatedFilesToCheck: generatedFilesToCheck, skipDirectories: true);
        }
    }
}
