using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.Serialization;
using System.Xml.Linq;
#if !NETCORE
using System.Web.Script.Serialization;
#endif
using NUnit.Framework;
using ServiceStack.Common.Tests.Models;
using ServiceStack.DataAnnotations;
using ServiceStack.Text.Tests.DynamicModels;
using ServiceStack.Web;

namespace ServiceStack.Text.Tests;

public class User
{
    public string FirstName { get; set; }
    [DataMember]
    public string LastName { get; set; }
    public Car Car { get; set; }
}

public class UserFields
{
    public string FirstName;
    public string LastName;
    public Car Car;
}

public class SubUser : User { }
public class SubUserFields : UserFields { }

public class Car
{
    public string Name { get; set; }
    public int Age { get; set; }
}

public class SubCar : Car
{
    public string Custom { get; set; }
}

public class CarWithSpecs : Car
{
    public List<CarSpec> Specs { get; set; }
}

public class CarSpec
{
    public string Item { get; set; }
    public string Value { get; set; }
}

public class UserDto
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Car { get; set; }
}

public class UsersData
{
    public int Id { get; set; }
        
    public User User { get; set; }
    public List<User> UsersList { get; set; } = new List<User>();
    public Dictionary<int, User> UsersMap { get; set; } = new Dictionary<int, User>();
}
public class UsersDto
{
    public int Id { get; set; }

    public UserDto User { get; set; }
    public List<UserDto> UsersList { get; set; } = new List<UserDto>();
    public Dictionary<int, UserDto> UsersMap { get; set; } = new Dictionary<int, UserDto>();
}

public enum Color
{
    Red,
    Green,
    Blue
}

public enum OtherColor
{
    Red,
    Green,
    Blue
}


public class IntNullableId
{
    public int? Id { get; set; }
}

public class IntId
{
    public int Id { get; set; }
}

public class BclTypes
{
    public int Int { get; set; }
    public long Long { get; set; }
    public double Double { get; set; }
    public decimal Decimal { get; set; }
}

public class BclTypeStrings
{
    public string Int { get; set; }
    public string Long { get; set; }
    public string Double { get; set; }
    public string Decimal { get; set; }
}

public class NullableConversion
{
    public decimal Amount { get; set; }
}

public class NullableConversionDto
{
    public decimal? Amount { get; set; }
}

public class DtoWithEnum
{
    public string Name { get; set; }
    public Color Color { get; set; }
}

public class NullableEnumConversion
{
    public Color Color { get; set; }
}

public class ReallyNullableEnumConversion
{
    public Color? Color { get; set; }
}

public class EnumConversion
{
    public Color Color { get; set; }
    public Color? NullableColor { get; set; }
}

public class NullableEnumConversionDto
{
    public OtherColor? Color { get; set; }
}

public class EnumConversionDto
{
    public OtherColor Color { get; set; }
}

public class EnumConversionString
{
    public string Color { get; set; }
    public string NullableColor { get; set; }
}

public class EnumConversionInt
{
    public int Color { get; set; }
    public int? NullableColor { get; set; }
}

public class ModelWithEnumerable
{
    public IEnumerable<User> Collection { get; set; }
}

public class ModelWithList
{
    public List<User> Collection { get; set; }
}

public class ModelWithArray
{
    public User[] Collection { get; set; }
}

public class ModelWithHashSet
{
    public HashSet<User> Collection { get; set; }
}

public class ModelWithIgnoredFields
{
    public int Id { get; set; }
    public string Name { get; set; }

    [ReadOnly]
    public int Ignored { get; set; }
}

public class ReadOnlyAttribute : AttributeBase { }
    
[EnumAsInt]
public enum MyEnumAsInt
{
    ZeroValue = 0,
    OneValue = 1,
    DefaultValue = 2,
};

class MyEnumAsIntSource
{
    public MyEnumAsInt MyEnumAsInt { get; set; } = MyEnumAsInt.DefaultValue;
}

class MyEnumAsIntTarget
{
    public MyEnumAsInt MyEnumAsInt { get; set; } = MyEnumAsInt.DefaultValue;
}

[TestFixture]
public class AutoMappingTests
{
    [Test]
    public void Can_convert_Default_Enum_Values()
    {
        var from = new MyEnumAsIntSource { MyEnumAsInt = MyEnumAsInt.ZeroValue };
        var to = from.ConvertTo<MyEnumAsIntTarget>();
            
        Assert.That(to.MyEnumAsInt, Is.EqualTo(from.MyEnumAsInt));
    }
        
    [Test]
    public void Does_populate()
    {
        var user = new User {
            FirstName = "Demis",
            LastName = "Bellot",
            Car = new Car { Name = "BMW X6", Age = 3 }
        };

        var userDto = new UserDto().PopulateWith(user);

        Assert.That(userDto.FirstName, Is.EqualTo(user.FirstName));
        Assert.That(userDto.LastName, Is.EqualTo(user.LastName));
        Assert.That(userDto.Car, Is.EqualTo("{Name:BMW X6,Age:3}"));
    }

    [Test]
    public void Does_translate()
    {
        var user = new User
        {
            FirstName = "Demis",
            LastName = "Bellot",
            Car = new Car { Name = "BMW X6", Age = 3 }
        };

        var userDto = user.ConvertTo<UserDto>();

        Assert.That(userDto.FirstName, Is.EqualTo(user.FirstName));
        Assert.That(userDto.LastName, Is.EqualTo(user.LastName));
        Assert.That(userDto.Car, Is.EqualTo("{Name:BMW X6,Age:3}"));
    }

    [Test]
    public void Does_enumstringconversion_translate()
    {
        var conversion = new EnumConversion { Color = Color.Blue };
        var conversionDto = conversion.ConvertTo<EnumConversionString>();

        Assert.That(conversionDto.Color, Is.EqualTo("Blue"));
    }

    [Test]
    public void Does_convert_to_EnumConversionInt()
    {
        var conversion = new EnumConversion
        {
            Color = Color.Green,
            NullableColor = Color.Green,
        };
        var conversionDto = conversion.ConvertTo<EnumConversionInt>();

        Assert.That(conversionDto.Color, Is.EqualTo(1));
        Assert.That(conversionDto.NullableColor, Is.EqualTo(1));
    }

    [Test]
    public void Does_convert_from_EnumConversionInt()
    {
        var conversion = new EnumConversionInt
        {
            Color = 1,
            NullableColor = 1,
        };
        var conversionDto = conversion.ConvertTo<EnumConversion>();

        Assert.That(conversionDto.Color, Is.EqualTo(Color.Green));
        Assert.That(conversionDto.NullableColor, Is.EqualTo(Color.Green));
    }

    [Test]
    public void Does_convert_to_EnumConversionString()
    {
        var conversion = new EnumConversion
        {
            Color = Color.Green,
            NullableColor = Color.Green,
        };
        var conversionDto = conversion.ConvertTo<EnumConversionString>();

        Assert.That(conversionDto.Color, Is.EqualTo("Green"));
        Assert.That(conversionDto.NullableColor, Is.EqualTo("Green"));
    }

    [Test]
    public void Does_convert_from_EnumConversionString()
    {
        var conversion = new EnumConversionString
        {
            Color = "Green",
            NullableColor = "Green",
        };
        var conversionDto = conversion.ConvertTo<EnumConversion>();

        Assert.That(conversionDto.Color, Is.EqualTo(Color.Green));
        Assert.That(conversionDto.NullableColor, Is.EqualTo(Color.Green));
    }

    [Test]
    public void Does_nullableconversion_translate()
    {
        var conversion = new NullableConversion { Amount = 123.45m };
        var conversionDto = conversion.ConvertTo<NullableConversionDto>();

        Assert.That(conversionDto.Amount, Is.EqualTo(123.45m));
    }

    [Test]
    public void Does_Enumnullableconversion_translate()
    {
        var conversion = new NullableEnumConversion { Color = Color.Green };
        var conversionDto = conversion.ConvertTo<NullableEnumConversionDto>();

        Assert.That(conversionDto.Color, Is.EqualTo(OtherColor.Green));
    }

    [Test]
    public void Does_Enumconversion_translate()
    {
        var conversion = new NullableEnumConversion { Color = Color.Green };
        var conversionDto = conversion.ConvertTo<EnumConversionDto>();

        Assert.That(conversionDto.Color, Is.EqualTo(OtherColor.Green));
    }

    [Test]
    public void Does_ReallyEnumnullableconversion_translate()
    {
        var conversion = new ReallyNullableEnumConversion { Color = Color.Green };
        var conversionDto = conversion.ConvertTo<NullableEnumConversionDto>();

        Assert.That(conversionDto.Color, Is.EqualTo(OtherColor.Green));
    }

    [Test]
    public void Does_RealyEnumconversion_translate()
    {
        var conversion = new ReallyNullableEnumConversion { Color = Color.Green };
        var conversionDto = conversion.ConvertTo<EnumConversionDto>();

        Assert.That(conversionDto.Color, Is.EqualTo(OtherColor.Green));
    }

    [Test]
    public void Does_Enumconversion_translateFromNull()
    {
        var conversion = new ReallyNullableEnumConversion { Color = null };
        var conversionDto = conversion.ConvertTo<EnumConversionDto>();

        Assert.That(conversionDto.Color, Is.EqualTo(default(OtherColor)));
    }

    [Test]
    public void Does_translate_nullableInt_to_and_from()
    {
        var nullable = new IntNullableId();

        var nonNullable = nullable.ConvertTo<IntId>();

        nonNullable.Id = 10;

        var expectedNullable = nonNullable.ConvertTo<IntNullableId>();

        Assert.That(expectedNullable.Id.Value, Is.EqualTo(nonNullable.Id));
    }

    [Test]
    public void Does_translate_from_properties_to_fields()
    {
        var user = new User
        {
            FirstName = "Demis",
            LastName = "Bellot",
            Car = new Car { Name = "BMW X6", Age = 3 }
        };

        var to = user.ConvertTo<UserFields>();
        Assert.That(to.FirstName, Is.EqualTo(user.FirstName));
        Assert.That(to.LastName, Is.EqualTo(user.LastName));
        Assert.That(to.Car.Name, Is.EqualTo(user.Car.Name));
        Assert.That(to.Car.Age, Is.EqualTo(user.Car.Age));
    }

    [Test]
    public void Does_translate_from_fields_to_properties()
    {
        var user = new UserFields
        {
            FirstName = "Demis",
            LastName = "Bellot",
            Car = new Car { Name = "BMW X6", Age = 3 }
        };

        var to = user.ConvertTo<UserFields>();
        Assert.That(to.FirstName, Is.EqualTo(user.FirstName));
        Assert.That(to.LastName, Is.EqualTo(user.LastName));
        Assert.That(to.Car.Name, Is.EqualTo(user.Car.Name));
        Assert.That(to.Car.Age, Is.EqualTo(user.Car.Age));
    }

    [Test]
    public void Does_translate_from_inherited_propeties()
    {
        var user = new SubUser
        {
            FirstName = "Demis",
            LastName = "Bellot",
            Car = new Car { Name = "BMW X6", Age = 3 }
        };

        var to = user.ConvertTo<UserFields>();
        Assert.That(to.FirstName, Is.EqualTo(user.FirstName));
        Assert.That(to.LastName, Is.EqualTo(user.LastName));
        Assert.That(to.Car.Name, Is.EqualTo(user.Car.Name));
        Assert.That(to.Car.Age, Is.EqualTo(user.Car.Age));
    }

    [Test]
    public void Does_translate_to_inherited_propeties()
    {
        var user = new User
        {
            FirstName = "Demis",
            LastName = "Bellot",
            Car = new Car { Name = "BMW X6", Age = 3 }
        };

        var to = user.ConvertTo<SubUserFields>();
        Assert.That(to.FirstName, Is.EqualTo(user.FirstName));
        Assert.That(to.LastName, Is.EqualTo(user.LastName));
        Assert.That(to.Car.Name, Is.EqualTo(user.Car.Name));
        Assert.That(to.Car.Age, Is.EqualTo(user.Car.Age));
    }

    [Test]
    public void Can_convert_BclTypes()
    {
        Assert.That("from".ConvertTo<string>(), Is.EqualTo("from"));
        Assert.That(1.ConvertTo<long>(), Is.EqualTo(1L));
        Assert.That(2L.ConvertTo<int>(), Is.EqualTo(2));
        Assert.That(3.3d.ConvertTo<float>(), Is.EqualTo(3.3f));
        Assert.That(4.4d.ConvertTo<decimal>(), Is.EqualTo(4.4m));
    }

    [Test]
    public void Does_coerce_from_BclTypes_to_strings()
    {
        var from = new BclTypes
        {
            Int = 1,
            Long = 2,
            Double = 3.3,
            Decimal = 4.4m,
        };

        var to = from.ConvertTo<BclTypeStrings>();
        Assert.That(to.Int, Is.EqualTo("1"));
        Assert.That(to.Long, Is.EqualTo("2"));
        Assert.That(to.Double, Is.EqualTo("3.3"));
        Assert.That(to.Decimal, Is.EqualTo("4.4"));
    }

    [Test]
    public void Does_coerce_from_strings_to_BclTypes()
    {
        var from = new BclTypeStrings
        {
            Int = "1",
            Long = "2",
            Double = "3.3",
            Decimal = "4.4",
        };

        var to = from.ConvertTo<BclTypes>();
        Assert.That(to.Int, Is.EqualTo(1));
        Assert.That(to.Long, Is.EqualTo(2));
        Assert.That(to.Double, Is.EqualTo(3.3d));
        Assert.That(to.Decimal, Is.EqualTo(4.4m));
    }

    [Test]
    public void Does_map_only_properties_with_specified_Attribute()
    {
        var user = new User
        {
            FirstName = "Demis",
            LastName = "Bellot",
            Car = new Car { Name = "BMW X6", Age = 3 }
        };

        var to = new User();
        to.PopulateFromPropertiesWithAttribute(user, typeof(DataMemberAttribute));

        Assert.That(to.LastName, Is.EqualTo(user.LastName));
        Assert.That(to.FirstName, Is.Null);
        Assert.That(to.Car, Is.Null);
    }

    [Test]
    public void Does_convert_ModelWithAllTypes()
    {
        var to = ModelWithAllTypes.Create(1);
        var from = to.ConvertTo<ModelWithAllTypes>();

        Assert.That(to.Equals(from));
    }

    public bool MatchesUsers(IEnumerable<User> u1s, IEnumerable<User> u2s)
    {
        if (u1s == null || u2s == null)
            return false;

        var u1sList = u1s.ToList();
        var u2sList = u2s.ToList();

        if (u1sList.Count != u2sList.Count)
            return false;

        for (var i = 0; i < u1sList.Count; i++)
        {
            var u1 = u1sList[i];
            var u2 = u2sList[i];

            if (u1.FirstName != u2.FirstName)
                return false;
            if (u1.LastName != u2.LastName)
                return false;
            if (u1.Car.Name != u2.Car.Name)
                return false;
            if (u1.Car.Age != u2.Car.Age)
                return false;
        }

        return true;
    }

    [Test]
    public void Does_convert_models_with_collections()
    {
        var from = new ModelWithEnumerable
        {
            Collection = new[] {
                new User { FirstName = "First1", LastName = "Last1", Car = new Car { Name = "Car1", Age = 1} },
                new User { FirstName = "First2", LastName = "Last2", Car = new Car { Name = "Car2", Age = 2} },
            }
        };

        Assert.That(MatchesUsers(from.Collection, from.ConvertTo<ModelWithEnumerable>().Collection));
        Assert.That(MatchesUsers(from.Collection, from.ConvertTo<ModelWithList>().Collection));
        Assert.That(MatchesUsers(from.Collection, from.ConvertTo<ModelWithArray>().Collection));
        Assert.That(MatchesUsers(from.Collection, from.ConvertTo<ModelWithHashSet>().Collection));

        Assert.That(MatchesUsers(from.Collection, from.Collection.ConvertTo<IEnumerable<User>>()));
        Assert.That(MatchesUsers(from.Collection, from.Collection.ConvertTo<List<User>>()));
        Assert.That(MatchesUsers(from.Collection, from.Collection.ConvertTo<User[]>()));
        Assert.That(MatchesUsers(from.Collection, from.Collection.ConvertTo<HashSet<User>>()));

        var array = from.Collection.ToArray();
        Assert.That(MatchesUsers(array, from.Collection.ConvertTo<IEnumerable<User>>()));
        Assert.That(MatchesUsers(array, from.Collection.ConvertTo<List<User>>()));
        Assert.That(MatchesUsers(array, from.Collection.ConvertTo<User[]>()));
        Assert.That(MatchesUsers(array, from.Collection.ConvertTo<HashSet<User>>()));

        var hashset = from.Collection.ToSet();
        Assert.That(MatchesUsers(hashset, from.Collection.ConvertTo<IEnumerable<User>>()));
        Assert.That(MatchesUsers(hashset, from.Collection.ConvertTo<List<User>>()));
        Assert.That(MatchesUsers(hashset, from.Collection.ConvertTo<User[]>()));
        Assert.That(MatchesUsers(hashset, from.Collection.ConvertTo<HashSet<User>>()));
    }

    public class Customer
    {
        public CompanyInfo CompanyInfo { get; set; }
        // other properties
    }

    public class CustomerDto
    {
        public CompanyInfoDto CompanyInfo { get; set; }
    }

    public class CompanyInfo
    {
        public int Id { get; set; }
        public string ITN { get; set; }
    }

    public class CompanyInfoDto
    {
        public int Id { get; set; }
        public string ITN { get; set; }
    }

    [Test]
    public void Does_retain_null_properties()
    {
        var user = new User { FirstName = "Foo" };
        var userDto = user.ConvertTo<UserFields>();

        Assert.That(userDto.FirstName, Is.EqualTo("Foo"));
        Assert.That(userDto.LastName, Is.Null);
        Assert.That(userDto.Car, Is.Null);

        var customer = new Customer { CompanyInfo = null };
        var dto = customer.ConvertTo<CustomerDto>();

        Assert.That(dto.CompanyInfo, Is.Null);
    }

    [Test]
    public void Does_ignore_properties_without_attributes()
    {
        var model = new ModelWithIgnoredFields
        {
            Id = 1,
            Name = "Foo",
            Ignored = 2
        };

        var dto = new ModelWithIgnoredFields { Ignored = 10 }
            .PopulateFromPropertiesWithoutAttribute(model, typeof(ReadOnlyAttribute));

        Assert.That(dto.Id, Is.EqualTo(model.Id));
        Assert.That(dto.Name, Is.EqualTo(model.Name));
        Assert.That(dto.Ignored, Is.EqualTo(10));
    }

#if !NETCORE
    public class IgnoredModel
    {
        public int Id { get; set; }

        [IgnoreDataMember]
        public int DataMemberIgnoreId { get; set; }

        [JsonIgnore]
        public int JsonIgnoreId { get; set; }

        [ScriptIgnore]
        public int ScriptIgnoreId { get; set; }
    }

    //Matches JSON.NET's [JsonIgnore] by name
    public class JsonIgnoreAttribute : AttributeBase { }

    [Test]
    public void Can_change_ignored_properties()
    {
        JsConfig.IgnoreAttributesNamed = JsConfig.IgnoreAttributesNamed.NewArray(
            with: typeof(ScriptIgnoreAttribute).Name,
            without: typeof(JsonIgnoreAttribute).Name);

        var dto = new IgnoredModel { JsonIgnoreId = 1, ScriptIgnoreId = 2 };

        Assert.That(dto.ToJson(), Is.EqualTo("{\"Id\":0,\"JsonIgnoreId\":1}"));
    }
#endif

    [Test]
    public void Does_convert_to_ValueType()
    {
        Assert.That("1".ConvertTo(typeof(int)), Is.EqualTo(1));
        Assert.That("1".ConvertTo(typeof(long)), Is.EqualTo(1L));
        Assert.That("1.1".ConvertTo(typeof(float)), Is.EqualTo(1.1f));
        Assert.That("1.1".ConvertTo(typeof(double)), Is.EqualTo(1.1d));
        Assert.That("1.1".ConvertTo(typeof(decimal)), Is.EqualTo(1.1M));

        Assert.That("2001-01-01".ConvertTo<DateTime>(), Is.EqualTo(new DateTime(2001, 01, 01)));
        Assert.That("98ece8400be4452eb6ad7c3a4404f119".ConvertTo<Guid>(), Is.EqualTo(new Guid("98ece8400be4452eb6ad7c3a4404f119")));
    }

    [Test]
    public void Does_convert_from_ValueType_to_strings()
    {
        Assert.That(1.ConvertTo(typeof(string)), Is.EqualTo("1"));
        Assert.That(1L.ConvertTo(typeof(string)), Is.EqualTo("1"));
        Assert.That(1.1f.ConvertTo(typeof(string)), Is.EqualTo("1.1"));
        Assert.That(1.1d.ConvertTo(typeof(string)), Is.EqualTo("1.1"));
        Assert.That(1.1M.ConvertTo(typeof(string)), Is.EqualTo("1.1"));
            
        Assert.That(new DateTime(2001, 01, 01).ConvertTo<string>(), Is.EqualTo("2001-01-01"));
        Assert.That(new Guid("98ECE840-0BE4-452E-B6AD-7C3A4404F119").ConvertTo<string>(), Is.EqualTo("98ece8400be4452eb6ad7c3a4404f119"));
    }

    [Test]
    public void Can_convert_from_List_object()
    {
        var from = 3.Times(i => (object)new Car { Age = i, Name = "Name" + i });
        var to = (List<Car>)TranslateListWithElements.TryTranslateCollections(
            typeof(List<object>), typeof(List<Car>), from);

        Assert.That(to.Count, Is.EqualTo(3));
        Assert.That(to[0].Age, Is.EqualTo(0));
        Assert.That(to[0].Name, Is.EqualTo("Name0"));
    }

    [Test]
    public void Can_convert_from_List_SubType()
    {
        var from = 3.Times(i => new SubCar { Age = i, Name = "Name" + i });
        var to = (List<Car>)TranslateListWithElements.TryTranslateCollections(
            typeof(List<SubCar>), typeof(List<Car>), from);

        Assert.That(to.Count, Is.EqualTo(3));
        Assert.That(to[0].Age, Is.EqualTo(0));
        Assert.That(to[0].Name, Is.EqualTo("Name0"));
    }

    [Test]
    public void Can_create_Dictionary_default_value()
    {
        var obj = (Dictionary<string, ClassWithEnum>)AutoMappingUtils.CreateDefaultValue(typeof(Dictionary<string, ClassWithEnum>), new Dictionary<Type, int>());
        Assert.That(obj, Is.Not.Null);
    }

    [Test]
    public void Can_get_generic_types_from_Dictionary_or_KVPs()
    {
        typeof(Dictionary<string, object>).GetKeyValuePairsTypes(out var keyType, out var valueType);
        Assert.That(keyType, Is.EqualTo(typeof(string)));
        Assert.That(valueType, Is.EqualTo(typeof(object)));
            
        typeof(IEnumerable<KeyValuePair<string, object>>).GetKeyValuePairsTypes(out keyType, out valueType);
        Assert.That(keyType, Is.EqualTo(typeof(string)));
        Assert.That(valueType, Is.EqualTo(typeof(object)));
            
        typeof(List<KeyValuePair<string, object>>).GetKeyValuePairsTypes(out keyType, out valueType);
        Assert.That(keyType, Is.EqualTo(typeof(string)));
        Assert.That(valueType, Is.EqualTo(typeof(object)));
    }

    [Test]
    public void Can_convert_between_different_types_of_Dictionaries_and_KVP_values()
    {
        var genericMap = new Dictionary<string, object> {
            { "a", 1 }
        };
        var stringMap = new Dictionary<string, string> {
            {"a", "1"}
        };
        var intMap = new Dictionary<string, int> {
            {"a", 1}
        };
        var kvps = new List<KeyValuePair<string, object>> {
            new KeyValuePair<string, object>("a", 1)
        };
        var stringKvps = new List<KeyValuePair<string, string>> {
            new KeyValuePair<string, string>("a", "1")
        };
        var objDict = new ObjectDictionary {
            { "a", 1 }
        };
            
        var genericMapStringValue = new Dictionary<string, object> {
            { "a", "1" }
        };
        var objDictStringValue = new ObjectDictionary {
            { "a", "1" }
        };
        var kvpsStringValue = new List<KeyValuePair<string, object>> {
            new KeyValuePair<string, object>("a", "1")
        };
            
        Assert.That(genericMap.ConvertTo<Dictionary<string,object>>(), Is.EquivalentTo(genericMap));
        Assert.That(genericMap.ConvertTo<Dictionary<string,string>>(), Is.EquivalentTo(stringMap));
        Assert.That(genericMap.ConvertTo<Dictionary<string,int>>(), Is.EquivalentTo(intMap));
        Assert.That(genericMap.ConvertTo<List<KeyValuePair<string, object>>>(), Is.EquivalentTo(kvps));
        Assert.That(genericMap.ConvertTo<List<KeyValuePair<string, string>>>(), Is.EquivalentTo(stringKvps));
        Assert.That(genericMap.ConvertTo<ObjectDictionary>(), Is.EquivalentTo(objDict));
            
        Assert.That(stringMap.ConvertTo<Dictionary<string,object>>(), Is.EquivalentTo(genericMapStringValue));
        Assert.That(stringMap.ConvertTo<Dictionary<string,string>>(), Is.EquivalentTo(stringMap));
        Assert.That(stringMap.ConvertTo<Dictionary<string,int>>(), Is.EquivalentTo(intMap));
        Assert.That(stringMap.ConvertTo<List<KeyValuePair<string, object>>>(), Is.EquivalentTo(kvpsStringValue));
        Assert.That(stringMap.ConvertTo<List<KeyValuePair<string, string>>>(), Is.EquivalentTo(stringKvps));
        Assert.That(stringMap.ConvertTo<ObjectDictionary>(), Is.EquivalentTo(objDictStringValue));
            
        Assert.That(intMap.ConvertTo<Dictionary<string,object>>(), Is.EquivalentTo(genericMap));
        Assert.That(intMap.ConvertTo<Dictionary<string,string>>(), Is.EquivalentTo(stringMap));
        Assert.That(intMap.ConvertTo<Dictionary<string,int>>(), Is.EquivalentTo(intMap));
        Assert.That(intMap.ConvertTo<List<KeyValuePair<string, object>>>(), Is.EquivalentTo(kvps));
        Assert.That(intMap.ConvertTo<List<KeyValuePair<string, string>>>(), Is.EquivalentTo(stringKvps));
        Assert.That(intMap.ConvertTo<ObjectDictionary>(), Is.EquivalentTo(objDict));
            
        Assert.That(kvps.ConvertTo<Dictionary<string,object>>(), Is.EquivalentTo(genericMap));
        Assert.That(kvps.ConvertTo<Dictionary<string,string>>(), Is.EquivalentTo(stringMap));
        Assert.That(kvps.ConvertTo<Dictionary<string,int>>(), Is.EquivalentTo(intMap));
        Assert.That(kvps.ConvertTo<List<KeyValuePair<string, object>>>(), Is.EquivalentTo(kvps));
        Assert.That(kvps.ConvertTo<List<KeyValuePair<string, string>>>(), Is.EquivalentTo(stringKvps));
        Assert.That(kvps.ConvertTo<ObjectDictionary>(), Is.EquivalentTo(objDict));
            
        Assert.That(stringKvps.ConvertTo<Dictionary<string,object>>(), Is.EquivalentTo(genericMapStringValue));
        Assert.That(stringKvps.ConvertTo<Dictionary<string,string>>(), Is.EquivalentTo(stringMap));
        Assert.That(stringKvps.ConvertTo<Dictionary<string,int>>(), Is.EquivalentTo(intMap));
        Assert.That(stringKvps.ConvertTo<List<KeyValuePair<string, object>>>(), Is.EquivalentTo(kvpsStringValue));
        Assert.That(stringKvps.ConvertTo<List<KeyValuePair<string, string>>>(), Is.EquivalentTo(stringKvps));
        Assert.That(stringKvps.ConvertTo<ObjectDictionary>(), Is.EquivalentTo(objDictStringValue));
    }

    [Test]
    public void Can_convert_between_different_types_of_Dictionaries_and_KVP_keys()
    {
        var genericMap = new Dictionary<object, string> {
            { 1, "a" }
        };
        var stringMap = new Dictionary<string, string> {
            {"1","a"}
        };
        var intMap = new Dictionary<int, string> {
            {1,"a"}
        };
        var kvps = new List<KeyValuePair<object, string>> {
            new KeyValuePair<object,string>(1, "a")
        };
        var stringKvps = new List<KeyValuePair<string, string>> {
            new KeyValuePair<string, string>("1","a")
        };
            
        var genericMapStringValue = new Dictionary<object,string> {
            { "1","a" }
        };
        var kvpsStringValue = new List<KeyValuePair<object,string>> {
            new KeyValuePair<object,string>("1","a")
        };
            
        Assert.That(genericMap.ConvertTo<Dictionary<object,string>>(), Is.EquivalentTo(genericMap));
        Assert.That(genericMap.ConvertTo<Dictionary<string,string>>(), Is.EquivalentTo(stringMap));
        Assert.That(genericMap.ConvertTo<Dictionary<int,string>>(), Is.EquivalentTo(intMap));
        Assert.That(genericMap.ConvertTo<List<KeyValuePair<object,string>>>(), Is.EquivalentTo(kvps));
        Assert.That(genericMap.ConvertTo<List<KeyValuePair<string,string>>>(), Is.EquivalentTo(stringKvps));
            
        Assert.That(stringMap.ConvertTo<Dictionary<object,string>>(), Is.EquivalentTo(genericMapStringValue));
        Assert.That(stringMap.ConvertTo<Dictionary<string,string>>(), Is.EquivalentTo(stringMap));
        Assert.That(stringMap.ConvertTo<Dictionary<int,string>>(), Is.EquivalentTo(intMap));
        Assert.That(stringMap.ConvertTo<List<KeyValuePair<object,string>>>(), Is.EquivalentTo(kvpsStringValue));
        Assert.That(stringMap.ConvertTo<List<KeyValuePair<string, string>>>(), Is.EquivalentTo(stringKvps));
            
        Assert.That(intMap.ConvertTo<Dictionary<object,string>>(), Is.EquivalentTo(genericMap));
        Assert.That(intMap.ConvertTo<Dictionary<string,string>>(), Is.EquivalentTo(stringMap));
        Assert.That(intMap.ConvertTo<Dictionary<int,string>>(), Is.EquivalentTo(intMap));
        Assert.That(intMap.ConvertTo<List<KeyValuePair<object,string>>>(), Is.EquivalentTo(kvps));
        Assert.That(intMap.ConvertTo<List<KeyValuePair<string, string>>>(), Is.EquivalentTo(stringKvps));
            
        Assert.That(kvps.ConvertTo<Dictionary<object,string>>(), Is.EquivalentTo(genericMap));
        Assert.That(kvps.ConvertTo<Dictionary<string,string>>(), Is.EquivalentTo(stringMap));
        Assert.That(kvps.ConvertTo<Dictionary<int,string>>(), Is.EquivalentTo(intMap));
        Assert.That(kvps.ConvertTo<List<KeyValuePair<object,string>>>(), Is.EquivalentTo(kvps));
        Assert.That(kvps.ConvertTo<List<KeyValuePair<string,string>>>(), Is.EquivalentTo(stringKvps));
            
        Assert.That(stringKvps.ConvertTo<Dictionary<object,string>>(), Is.EquivalentTo(genericMapStringValue));
        Assert.That(stringKvps.ConvertTo<Dictionary<string,string>>(), Is.EquivalentTo(stringMap));
        Assert.That(stringKvps.ConvertTo<Dictionary<int,string>>(), Is.EquivalentTo(intMap));
        Assert.That(stringKvps.ConvertTo<List<KeyValuePair<object,string>>>(), Is.EquivalentTo(kvpsStringValue));
        Assert.That(stringKvps.ConvertTo<List<KeyValuePair<string,string>>>(), Is.EquivalentTo(stringKvps));
    }

    class HasObject
    {
        public object Value { get; set; }
    }

    [Test]
    public void Can_convert_dictionary_to_HasObject()
    {
        var objDict = new ObjectDictionary {
            ["value"] = new ObjectDictionary {
                ["a"] = 1
            }
        };

        var to = objDict.FromObjectDictionary<HasObject>();
        Assert.That(to.Value, Is.EqualTo(objDict["value"]));
            
        var kvps = new ObjectDictionary {
            ["value"] = new List<KeyValuePair<string,int>> {
                new KeyValuePair<string, int>("a",1)
            }
        };
            
        to = objDict.FromObjectDictionary<HasObject>();
        Assert.That(to.Value, Is.EqualTo(objDict["value"]));
    }
        
}

public enum ClassWithEnumType
{
    One = 1,
    Two = 2,
    Three = 3
}

public class ClassWithEnum
{
    public ClassWithEnumType Type { get; set; }
}    

public class Test
{
    public string Name { get; set; }
}

public class PropertyExpressionTests
{
    [Test]
    public void Can_call_typed_setter_Expressions()
    {
        var nameProperty = typeof(Test).GetProperty("Name");
        var setMethod = nameProperty.GetSetMethod();

        var instance = Expression.Parameter(typeof(Test), "i");
        var argument = Expression.Parameter(typeof(string), "a");

        var setterCall = Expression.Call(instance, setMethod, argument);
        var fn = Expression.Lambda<Action<Test, string>>(setterCall, instance, argument).Compile();

        var test = new Test();
        fn(test, "Foo");
        Assert.That(test.Name, Is.EqualTo("Foo"));
    }

    [Test]
    public void Can_call_object_setter_Expressions()
    {
        var nameProperty = typeof(Test).GetProperty("Name");

        var instance = Expression.Parameter(typeof(object), "i");
        var argument = Expression.Parameter(typeof(object), "a");

        var instanceParam = Expression.Convert(instance, nameProperty.ReflectedType);
        var valueParam = Expression.Convert(argument, nameProperty.PropertyType);

        var setterCall = Expression.Call(instanceParam, nameProperty.GetSetMethod(nonPublic:true), valueParam);

        var fn = Expression.Lambda<Action<object, object>>(setterCall, instance, argument).Compile();

        var test = new Test();
        fn(test, "Foo");
        Assert.That(test.Name, Is.EqualTo("Foo"));
    }

    public class RawRequest : IRequiresRequestStream
    {
        public Stream RequestStream { get; set; }
    }

    [Test]
    public void Can_create_DTO_with_Stream()
    {
        var o = typeof(RawRequest).CreateInstance();
        var requestObj = AutoMappingUtils.PopulateWith(o);

        Assert.That(requestObj, Is.Not.Null);
    }

    public class ModelWithUri
    {
        public Uri Uri { get; set; }
    }
    public class ModelWithUriString
    {
        public string Uri { get; set; }
    }

    [Test]
    public void Does_map_Uri()
    {
        var dto = new ModelWithUri { Uri = new Uri("http://a.com") };

        var to = new ModelWithUri().PopulateWithNonDefaultValues(dto);
        Assert.That(to.Uri, Is.EqualTo(dto.Uri));

        var toString = new ModelWithUriString().PopulateWithNonDefaultValues(dto);
        Assert.That(toString.Uri, Is.EqualTo(dto.Uri.AbsoluteUri));
    }

    public class ViewModel
    {
        public string Public { get; set; }
            
        [IgnoreOnSelect]
        public string Private { get; set; }
    }
        
    [Test]
    public void Does_convert_same_model_without_Attributes()
    {
        var vm = new ViewModel().PopulateFromPropertiesWithoutAttribute(
            new ViewModel { Public = "Public", Private = "Private" },
            typeof(IgnoreOnSelectAttribute));
            
        Assert.That(vm.Public, Is.EqualTo("Public"));
        Assert.That(vm.Private, Is.Null);
    }

    [Test]
    public void Can_use_ToObjectDictionary_to_Remove_Property_with_Attribute()
    {
        var vm = new ViewModel {Public = "Public", Private = "Private"};
        var map = vm.ToObjectDictionary();
        vm.GetType().GetProperties()
            .Where(x => x.HasAttribute<IgnoreOnSelectAttribute>())
            .Each(x => map.Remove(x.Name));

        vm = map.FromObjectDictionary<ViewModel>();
        Assert.That(vm.Public, Is.EqualTo("Public"));
        Assert.That(vm.Private, Is.Null);
    }

    class DictionaryTest
    {
        public string A { get; set; }
        public int B { get; set; }
        public bool C { get; set; }
        public double D { get; set; }
    }

    [Test]
    public void Can_convert_anonymous_object_to_ObjectDictionary()
    {
        var newObj = new { A = "a", B = 1, C = true, D = 2.0 };
        var type = new DictionaryTest { A = "a", B = 1, C = true, D = 2.0 };
        var expected = new Dictionary<string, object> {
            ["A"] = "a",
            ["B"] = 1,
            ["C"] = true,
            ["D"] = 2.0
        };

        var to = newObj.ToObjectDictionary();
        Assert.That(to, Is.EquivalentTo(expected));
        to = type.ToObjectDictionary();
        Assert.That(to, Is.EquivalentTo(expected));
        to = expected.ToObjectDictionary();
        Assert.That(to, Is.EquivalentTo(expected));
    }

    [Test]
    public void Can_convert_string_to_collection()
    {
        Assert.That("a,b,c".ConvertTo<string[]>(), Is.EquivalentTo(new[]{ "a", "b", "c" }));
        Assert.That("1,2,3".ConvertTo<int[]>(), Is.EquivalentTo(new[]{ 1, 2, 3 }));
    }
        
    [Test]
    public void Translate_Between_Models_of_different_types_and_nullables()
    {
        var fromObj = ModelWithFieldsOfDifferentTypes.CreateConstant(1);

        var toObj = fromObj.ConvertTo<ModelWithFieldsOfDifferentTypesAsNullables>();

        Console.WriteLine(toObj.Dump());

        ModelWithFieldsOfDifferentTypesAsNullables.AssertIsEqual(fromObj, toObj);
    }

    [Test]
    public void Translate_Between_Models_of_nullables_and_different_types()
    {
        var fromObj = ModelWithFieldsOfDifferentTypesAsNullables.CreateConstant(1);

        var toObj = fromObj.ConvertTo<ModelWithFieldsOfDifferentTypes>();

        Console.WriteLine(toObj.Dump());

        ModelWithFieldsOfDifferentTypesAsNullables.AssertIsEqual(toObj, fromObj);
    }

    [Test]
    public void Can_convert_to_array_to_string()
    {
        Assert.That(new []{ new ViewModel { Public = "A"} }.ConvertTo<string>(), Is.Not.Empty);
        Assert.That(new []{ DayOfWeek.Monday }.ConvertTo<string>(), Is.Not.Empty);
    }
        
    [Test]
    public void To_works_with_ValueTypes()
    {
        Assert.That(1.ToString().ConvertTo<int>(), Is.EqualTo(1));
    }

    [Test]
    public void To_on_null_or_empty_string_returns_default_value_supplied()
    {
        const string nullString = null;
        Assert.That("".ConvertTo(1), Is.EqualTo(1));
        Assert.That("".ConvertTo(default(int)), Is.EqualTo(default(int)));
        Assert.That(nullString.ConvertTo(1), Is.EqualTo(1));
    }

    [Test]
    public void To_ValueType_on_null_or_empty_string_returns_default_value()
    {
        Assert.That("".ConvertTo<int>(), Is.EqualTo(default(int)));
    }

    struct A
    {
        public int Id { get; }
        public A(int id) => Id = id;
        public static implicit operator B(A from) => new B(from.Id);
    }

    struct B
    {
        public int Id { get; }
        public B(int id) => Id = id;
        public static implicit operator A(B from) => new A(from.Id);
    }

    [Test]
    public void Can_convert_between_structs_with_implicit_casts()
    {
        var a = new A(1);
        var b = a;
        Assert.That(b.Id, Is.EqualTo(a.Id));

        b = a.ConvertTo<B>();
        Assert.That(b.Id, Is.EqualTo(a.Id));
    }

    [Test]
    public void Can_convert_from_class_implicit_Casts()
    {
        var to = "test".ConvertTo<XName>();
        Assert.That(to.LocalName, Is.EqualTo("test"));
    }

    struct C
    {
        public int Id { get; }
        public C(int id) => Id = id;
        public static explicit operator C(D from) => new C(from.Id);
    }

    struct D
    {
        public int Id { get; }
        public D(int id) => Id = id;
        public static explicit operator D(C from) => new D(from.Id);
    }

    [Test]
    public void Can_convert_between_structs_with_explicit_casts()
    {
        var c = new C(1);
        var d = (D)c;
        Assert.That(d.Id, Is.EqualTo(c.Id));

        d = c.ConvertTo<D>();
        Assert.That(d.Id, Is.EqualTo(c.Id));
    }
        
    [Test]
    public void Can_Convert_from_ObjectDictionary_Containing_Another_Object_Dictionary()
    {
        var map = new Dictionary<string, object>
        {
            { "name", "Foo" },
            { "otherNames", new List<object>
                {
                    new Dictionary<string, object> { { "name", "Fu" } },
                    new Dictionary<string, object> { { "name", "Fuey" } }
                }
            }
        };

        var fromDict = map.ConvertTo<AutoMappingObjectDictionaryTests.PersonWithIdentities>();

        Assert.That(fromDict.Name, Is.EqualTo("Foo"));
        Assert.That(fromDict.OtherNames.Count, Is.EqualTo(2));
        Assert.That(fromDict.OtherNames.First().Name, Is.EqualTo("Fu"));
        Assert.That(fromDict.OtherNames.Last().Name, Is.EqualTo("Fuey"));

        var toDict = fromDict.ConvertTo<Dictionary<string,object>>();
        Assert.That(toDict["Name"], Is.EqualTo("Foo"));
        Assert.That(toDict["OtherNames"], Is.EqualTo(fromDict.OtherNames));

        var toObjDict = fromDict.ConvertTo<ObjectDictionary>();
        Assert.That(toObjDict["Name"], Is.EqualTo("Foo"));
        Assert.That(toObjDict["OtherNames"], Is.EqualTo(fromDict.OtherNames));
    }

    class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }

        protected bool Equals(Person other) => Id == other.Id && string.Equals(Name, other.Name);

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            return obj.GetType() == this.GetType() && Equals((Person) obj);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                return (Id * 397) ^ (Name != null ? Name.GetHashCode() : 0);
            }
        }
    }

    [Test]
    public void Can_convert_between_Poco_and_ObjectDictionary()
    {
        var person = new Person { Id = 1, Name = "foo" };

        Assert.That(person.ConvertTo<Dictionary<string, object>>(), Is.EqualTo(new Dictionary<string, object> {
            {"Id", 1},
            {"Name", "foo"},
        })); 
        Assert.That(new Dictionary<string, object> {
            {"Id", 1},
            {"Name", "foo"},
        }.ConvertTo<Person>(), Is.EqualTo(person));

        Assert.That(person.ConvertTo<ObjectDictionary>(), Is.EqualTo(new ObjectDictionary {
            {"Id", 1},
            {"Name", "foo"},
        })); 
        Assert.That(new ObjectDictionary {
            {"Id", 1},
            {"Name", "foo"},
        }.ConvertTo<Person>(), Is.EqualTo(person));

        Assert.That(person.ConvertTo<Dictionary<string, string>>(), Is.EqualTo(new Dictionary<string, string> {
            {"Id", "1"},
            {"Name", "foo"},
        })); 

        Assert.That(new Dictionary<string, string> {
            {"Id", "1"},
            {"Name", "foo"},
        }.ConvertTo<Person>(), Is.EqualTo(person)); 

        Assert.That(person.ConvertTo<StringDictionary>(), Is.EqualTo(new StringDictionary {
            {"Id", "1"},
            {"Name", "foo"},
        })); 
        Assert.That(new StringDictionary {
            {"Id", "1"},
            {"Name", "foo"},
        }.ConvertTo<Person>(), Is.EqualTo(person)); 
    }

    [Test]
    public void Can_ToObjectDictionary_KVPs()
    {
        var objKvp = new KeyValuePair<string, object>("A", 1);
        var strKvp = new KeyValuePair<string, string>("A", "1");
        var intKvp = new KeyValuePair<string, int>("A", 1);
            
        Assert.That(objKvp.ToObjectDictionary(), Is.EquivalentTo(new Dictionary<string, object> { {"Key", "A" }, { "Value", 1  } } ));
        Assert.That(strKvp.ToObjectDictionary(), Is.EquivalentTo(new Dictionary<string, object> { {"Key", "A" }, { "Value", "1"} } ));
        Assert.That(intKvp.ToObjectDictionary(), Is.EquivalentTo(new Dictionary<string, object> { {"Key", "A" }, { "Value", 1  } } ));
            
        var objKvp2 = new KeyValuePair<string, object>("B", 2);
        var strKvp2 = new KeyValuePair<string, string>("B", "2");
        var intKvp2 = new KeyValuePair<string, int>("B", 2);

        Assert.That(new[] { objKvp, objKvp2 }.ToObjectDictionary(), Is.EquivalentTo(new Dictionary<string, object> { {"A", 1}, {"B", 2} }));
        Assert.That(new[] { strKvp, strKvp2 }.ToObjectDictionary(), Is.EquivalentTo(new Dictionary<string, object> { {"A", "1"}, {"B", "2"} }));
        Assert.That(new[] { intKvp, intKvp2 }.ToObjectDictionary(), Is.EquivalentTo(new Dictionary<string, object> { {"A", 1}, {"B", 2} }));
    }

    [Test]
    public void Can_convert_KVPs_to_ObjectDictionary()
    {
        var objKvp = new KeyValuePair<string, object>("A", 1);
        var strKvp = new KeyValuePair<string, string>("A", "1");
        var intKvp = new KeyValuePair<string, int>("A", 1);
            
        Assert.That(objKvp.ConvertTo<Dictionary<string,object>>(), Is.EquivalentTo(new Dictionary<string, object> { {"Key", "A" }, { "Value", 1  } } ));
        Assert.That(strKvp.ConvertTo<Dictionary<string,object>>(), Is.EquivalentTo(new Dictionary<string, object> { {"Key", "A" }, { "Value", "1"} } ));
        Assert.That(intKvp.ConvertTo<Dictionary<string,object>>(), Is.EquivalentTo(new Dictionary<string, object> { {"Key", "A" }, { "Value", 1  } } ));
            
        var objKvp2 = new KeyValuePair<string, object>("B", 2);
        var strKvp2 = new KeyValuePair<string, string>("B", "2");
        var intKvp2 = new KeyValuePair<string, int>("B", 2);

        Assert.That(new[] { objKvp, objKvp2 }.ConvertTo<Dictionary<string,object>>(), Is.EquivalentTo(new Dictionary<string, object> { {"A", 1}, {"B", 2} }));
        Assert.That(new[] { strKvp, strKvp2 }.ConvertTo<Dictionary<string,object>>(), Is.EquivalentTo(new Dictionary<string, object> { {"A", "1"}, {"B", "2"} }));
        Assert.That(new[] { intKvp, intKvp2 }.ConvertTo<Dictionary<string,object>>(), Is.EquivalentTo(new Dictionary<string, object> { {"A", 1}, {"B", 2} }));
    }

    [Test]
    public void Can_convert_between_KVP_Objects_and_IEnumerable()
    {
        var kvps = new List<KeyValuePair<string, object>> {
            new KeyValuePair<string, object>("A", 1),
            new KeyValuePair<string, object>("B", 2),
            new KeyValuePair<string, object>("C", 3),
        };
            
        Assert.That(kvps.ConvertTo<IEnumerable<object>>(), Is.EqualTo(kvps));
        Assert.That(kvps.ConvertTo<List<object>>(), Is.EqualTo(kvps));
        Assert.That(kvps.ConvertTo<object[]>(), Is.EqualTo(kvps));

        List<object> listObjs = kvps.Cast<object>().ToList();
        Assert.That(listObjs.ConvertTo<List<KeyValuePair<string, object>>>(), Is.EquivalentTo(listObjs));
        object[] arrayObjs = kvps.Cast<object>().ToArray();
        Assert.That(arrayObjs.ConvertTo<List<KeyValuePair<string, object>>>(), Is.EquivalentTo(arrayObjs));
    }

    [Test]
    public void Can_convert_between_KVP_Strings_and_IEnumerable()
    {
        var kvps = new List<KeyValuePair<string, string>> {
            new KeyValuePair<string, string>("A", "1"),
            new KeyValuePair<string, string>("B", "2"),
            new KeyValuePair<string, string>("C", "3"),
        };
            
        Assert.That(kvps.ConvertTo<IEnumerable<object>>(), Is.EqualTo(kvps));
        Assert.That(kvps.ConvertTo<List<object>>(), Is.EqualTo(kvps));
        Assert.That(kvps.ConvertTo<object[]>(), Is.EqualTo(kvps));

        List<object> listObjs = kvps.Cast<object>().ToList();
        Assert.That(listObjs.ConvertTo<List<KeyValuePair<string, object>>>(), Is.EquivalentTo(listObjs));
        object[] arrayObjs = kvps.Cast<object>().ToArray();
        Assert.That(arrayObjs.ConvertTo<List<KeyValuePair<string, object>>>(), Is.EquivalentTo(arrayObjs));
    }

    [Test]
    public void Can_convert_between_KVP_ints_and_IEnumerable()
    {
        var kvps = new List<KeyValuePair<string, int>> {
            new KeyValuePair<string, int>("A", 1),
            new KeyValuePair<string, int>("B", 2),
            new KeyValuePair<string, int>("C", 3),
        };
            
        Assert.That(kvps.ConvertTo<IEnumerable<object>>(), Is.EqualTo(kvps));
        Assert.That(kvps.ConvertTo<List<object>>(), Is.EqualTo(kvps));
        Assert.That(kvps.ConvertTo<object[]>(), Is.EqualTo(kvps));

        List<object> listObjs = kvps.Cast<object>().ToList();
        Assert.That(listObjs.ConvertTo<List<KeyValuePair<string, object>>>(), Is.EquivalentTo(listObjs));
        object[] arrayObjs = kvps.Cast<object>().ToArray();
        Assert.That(arrayObjs.ConvertTo<List<KeyValuePair<string, object>>>(), Is.EquivalentTo(arrayObjs));
    }

    public enum TestEnum { A, B, C }

    [Test]
    public void Can_convert_between_list_of_value_types()
    {
        var enumArrays = new[]{ TestEnum.A, TestEnum.B, TestEnum.C };
        var strArrays = new List<string> {"A","B","C"};
        Assert.That(enumArrays.ConvertTo<List<TestEnum>>(), Is.EquivalentTo(enumArrays.ToList()));
        Assert.That(enumArrays.ConvertTo<List<string>>(), Is.EquivalentTo(strArrays));

        var strNums = new List<string> {"1","2","3"};
        var intNums = new List<int> {1,2,3};
        Assert.That(strNums.ConvertTo<List<int>>(), Is.EquivalentTo(intNums));
        Assert.That(intNums.ConvertTo<List<string>>(), Is.EquivalentTo(strNums));
    }
        
    public class TestClassA
    {
        public IList<string> IListStrings { get; set; }
        public ArrayOfString ListStrings { get; set; }
        public IList<TestEnum> ListEnums { get; set; }
    }
    public class TestClassB
    {
        public ArrayOfString IListStrings { get; set; }
        public IList<string> ListStrings { get; set; }
        public ArrayOfString ListEnums { get; set; }
    }
    public class TestClassC
    {
        public IList<string> ListStrings { get; protected set; }
    }

    [Test]
    public void Can_translate_generic_lists()
    {
            
        var values = new[] { "A", "B", "C" };
        var testA = new TestClassA
        {
            IListStrings = new List<string>(values),
            ListStrings = new ArrayOfString(values),
            ListEnums = new List<TestEnum> { TestEnum.A, TestEnum.B, TestEnum.C },
        };

        var fromTestA = testA.ConvertTo<TestClassB>();

        AssertAreEqual(testA, fromTestA);

        var userFileTypeValues = testA.ListEnums.Map(x => x.ToString());
        var testB = new TestClassB
        {
            IListStrings = new ArrayOfString(values),
            ListStrings = new List<string>(values),
            ListEnums = new ArrayOfString(userFileTypeValues),
        };

        var fromTestB = testB.ConvertTo<TestClassA>();
        AssertAreEqual(fromTestB, testB);
    }
        
    private static void AssertAreEqual(TestClassA testA, TestClassB testB)
    {
        Assert.That(testA, Is.Not.Null);
        Assert.That(testB, Is.Not.Null);

        Assert.That(testA.IListStrings, Is.Not.Null);
        Assert.That(testB.IListStrings, Is.Not.Null);
        Assert.That(testA.IListStrings,
            Is.EquivalentTo(new List<string>(testB.IListStrings)));

        Assert.That(testA.ListStrings, Is.Not.Null);
        Assert.That(testB.ListStrings, Is.Not.Null);
        Assert.That(testA.ListStrings, Is.EquivalentTo(testB.ListStrings));

        Assert.That(testA.ListEnums, Is.Not.Null);
        Assert.That(testB.ListEnums, Is.Not.Null);
        Assert.That(testA.ListEnums,
            Is.EquivalentTo(testB.ListEnums.ConvertAll(x => x.ToEnum<TestEnum>())));
    }

    class Company
    {
        public string Name { get; set; }
        public Address Address { get; set; }
    }

    class Address
    {
        public string City { get; set; }
        public string PostCode { get; set; }
    }

    [Test]
    public void Can_register_converters_to_PopulateWithNonDefaultValues()
    {
        Company Create()
        {
            return new Company {
                Name = "Compnay1",
                Address = new Address {
                    City = "NY",
                    PostCode = "1234",
                }
            };
        }

        var defaults = Create();
        var original = Create();

        original.PopulateWithNonDefaultValues(new Company {
            Address = new Address {
                PostCode = "4321"
            }
        });
            
        Assert.That(original.Name, Is.EqualTo(defaults.Name));
        Assert.That(original.Address.City, Is.Null);
        Assert.That(original.Address.PostCode, Is.EqualTo("4321"));
            
        original = Create();

        var updated = new Company {
            Address = new Address {
                PostCode = "4321"
            }
        };
        original.Address.PopulateWithNonDefaultValues(updated.Address);
        updated.Address = null;
        original.PopulateWithNonDefaultValues(updated);
            
        Assert.That(original.Name, Is.EqualTo(defaults.Name));
        Assert.That(original.Address.City, Is.EqualTo(defaults.Address.City));
        Assert.That(original.Address.PostCode, Is.EqualTo("4321"));
    }
        
    public class NavItem : IMeta
    {
        public string Label { get; set; }
        public string Path { get; set; }
        
        /// <summary>
        /// Whether Active class should only be added when paths are exact match
        /// otherwise checks if ActivePath starts with Path
        /// </summary>
        public bool Exact { get; set; }

        public string Id { get; set; }    // Emit id="{Id}"
        public string Class { get; set; } // Override class="{Class}"
        
        public List<NavItem> Children { get; set; } = new List<NavItem>();
        
        public Dictionary<string, string> Meta { get; set; } = new Dictionary<string, string>();
    }

    [Test]
    public void Does_convert_nested_collections()
    {
        var to = new[] {
            new Dictionary<string,object> {
                ["id"] = "A1",
                ["children"] = new List<object>
                {
                    new Dictionary<string,object>
                    {
                        ["id"] = "B1",
                    },
                    new Dictionary<string,object>
                    {
                        ["id"] = "B2",
                        ["children"] = new List<object> {
                            new Dictionary<string,object>
                            {
                                ["id"] = "C1",
                            },
                        }
                    },
                }
            }
        };

        var navItems = to.ConvertTo<List<NavItem>>();
        Assert.That(navItems[0].Id, Is.EqualTo("A1"));
        Assert.That(navItems[0].Children[0].Id, Is.EqualTo("B1"));
        Assert.That(navItems[0].Children[1].Id, Is.EqualTo("B2"));
        Assert.That(navItems[0].Children[1].Children[0].Id, Is.EqualTo("C1"));
    }

    public class TestMethod
    {
        public void MyMethod()
        {
            MyProp = nameof(MyProp);
        }
        public string MyProp { get; set; }
    }
        
    public class TestMethod2
    {
        public void MyMethod(string myProp)
        {
            MyProp = nameof(MyMethod);
        }
        public string MyProp { get; set; }
    }
        
    [Test]
    public void Does_not_try_to_map_methods()
    {
        var from = new TestMethod();
        var to = new TestMethod2();
        from.MyProp = "Test";
        to.PopulateFromPropertiesWithoutAttribute(from, typeof(IgnoreDataMemberAttribute));
        Assert.That(from.MyProp, Is.EqualTo(to.MyProp));
    }

    [Test]
    public void Converts_StringDict_to_InterfaceStringDict_when_null()
    {
        var classBase = new StringDict
        {
            Dict = null
        };

        var converted = classBase.ConvertTo<InterfaceStringDict>();
        Assert.That(converted.ToJson(), Is.Not.Empty);
    }
    
    [Test]
    public void Converts_StringDict_to_InterfaceStringDict()
    {
        var classBase = new StringDict
        {
            Dict = new Dictionary<string, string>
            {
                ["key1"] = "value1",
                ["key2"] = "value2",
                ["key3"] = "value3"
            }
        };

        var converted = classBase.ConvertTo<InterfaceStringDict>();
        Assert.That(converted.ToJson(), Is.Not.Null.Or.Empty);
    }

    [Test]
    public void Converts_InterfaceStringDict()
    {
        var classBase = new InterfaceStringDict
        {
            Dict = new Dictionary<string, string>
            {
                ["key1"] = "value1",
                ["key2"] = "value2",
                ["key3"] = "value3"
            }
        };

        var converted = classBase.ConvertTo<InterfaceStringDict2>();
        Assert.That(converted.ToJson(), Is.Not.Null.Or.Empty);
    }

    [Test]
    public void Converts_InterfaceStringDict_when_null()
    {
        var classBase = new InterfaceStringDict
        {
            Dict = null
        };

        var converted = classBase.ConvertTo<InterfaceStringDict2>();
        Assert.That(converted.ToJson(), Is.Not.Null.Or.Empty);
    }

    [Test]
    public void Converts_StringDict_when_null()
    {
        var classBase = new StringDict
        {
            Dict = null
        };

        var converted = classBase.ConvertTo<StringDict2>();
        Assert.That(converted.ToJson(), Is.Not.Null.Or.Empty);
    }

    [Test]
    public void Converts_StringDict()
    {
        var classBase = new StringDict
        {
            Dict = new Dictionary<string, string>
            {
                ["key1"] = "value1",
                ["key2"] = "value2",
                ["key3"] = "value3"
            }
        };

        var converted = classBase.ConvertTo<StringDict2>();
        Assert.That(converted.ToJson(), Is.Not.Null.Or.Empty);
    }    

    public class StringDict
    {
        public Dictionary<string, string> Dict { get; set; }
    }

    public class StringDict2
    {
        public Dictionary<string, string> Dict { get; set; }
    }
    
    public class InterfaceStringDict
    {
        public IDictionary<string, string> Dict { get; set; }
    }

    public class InterfaceStringDict2
    {
        public IDictionary<string, string> Dict { get; set; }
    }

    public record struct RecordImplicit1
    {
        public int Value { get; set; }
        public static implicit operator RecordImplicit1(RecordImplicit2 from) => new() { Value = from.Value };
    }

    public record struct RecordImplicit2
    {
        public int Value { get; set; }
        public static implicit operator RecordImplicit2(RecordImplicit1 from) => new() { Value = from.Value };
    }
    
    [Test]
    public void Converts_Record_Structs_with_implicit_casts()
    {
        var x = new RecordImplicit1 { Value = 42 };
        var y = x.ConvertTo<RecordImplicit2>();
        Assert.That(x.Value, Is.EqualTo(y.Value));

        RecordImplicit1 a = new() { Value = 2 };
        RecordImplicit2 b = a;
        Assert.That(a.Value, Is.EqualTo(b.Value));
    }

    
    public struct RecordStruct1
    {
        public int Value { get; set; }
        public RecordStruct1(string value) => Value = int.Parse(value);
        public override string ToString() => $"{Value}";
    }

    public struct RecordStruct2
    {
        public int Value { get; set; }
        public RecordStruct2(string value) => Value = int.Parse(value);
        public override string ToString() => $"{Value}";
    }

    [Test]
    public void Converts_RecordStructs()
    {
        var x = new RecordStruct1 { Value = 42 };
        var y = x.ConvertTo<RecordStruct2>();
        Assert.That(x.Value, Is.EqualTo(y.Value));
    }

    class NullableSource
    {
        public int NonNullableId { get; set; }
        public int? NullableIdNonMatchingUnderlyingType { get; set; }
        public int? NullableIdMatchingUnderlyingType { get; set; }
        public DateTime? DateTime { get; set; }
        public bool NullableBool { get; set; }
    }
    class NullableTarget
    {
        public int NonNullableId { get; set; }
        public long? NullableIdNonMatchingUnderlyingType { get; set; }
        public int? NullableIdMatchingUnderlyingType { get; set; }
        public DateTime? NullableDateTime { get; set; }
        public DateTime DateTime { get; set; }
        public DateTime TargetOnlyDateTime { get; set; }
        public bool? NullableBool { get; set; }
    }

    [Test]
    public void Does_convert_nullables_of_different_types()
    {
        var source = new NullableSource();
        var target = source.ConvertTo<NullableTarget>();
        
        Assert.That(target.NonNullableId, Is.EqualTo(default(int)));
        Assert.That(target.NullableIdNonMatchingUnderlyingType, Is.Null);
        Assert.That(target.NullableIdMatchingUnderlyingType, Is.Null);
        Assert.That(target.NullableDateTime, Is.Null);
        Assert.That(target.DateTime, Is.EqualTo(default(DateTime)));
        Assert.That(target.TargetOnlyDateTime, Is.EqualTo(default(DateTime)));
    }

    [Test]
    public void Does_not_populate_bool_with_nullable_bool()
    {
        var to = new NullableSource {
            NullableBool = true,
        };

        var from = new NullableTarget();
        to.PopulateWithNonDefaultValues(from);

        Assert.That(to.NullableBool, Is.True);
    }
}
