using System;
using System.Collections.Generic;
using System.Linq;

using LinqToDB;
using LinqToDB.DataProvider.Firebird;
using LinqToDB.Mapping;

using NUnit.Framework;

namespace Tests.Linq
{
	using Model;

	[TestFixture]
	public class DynamicColumnsTests : TestBase
	{
		// Introduced to ensure that we process not only constants in column names
		static string IDColumn        = "ID";
		static string DiagnosisColumn = "Diagnosis";
		static string PatientColumn   = "Patient";

		[Test]
		public void SqlPropertyWithNonDynamicColumn([DataSources] string context)
		{
			using (var db = GetDataContext(context))
			{
				var result = db.GetTable<Person>()
					.Where(x => Sql.Property<int>(x, IDColumn) == 1)
					.ToList();

				Assert.AreEqual(1, result.Count);
				Assert.AreEqual("John", result.Single().FirstName);
			}
		}

		[Test]
		public void SqlPropertyWithNavigationalNonDynamicColumn([DataSources] string context)
		{
			using (var db = GetDataContext(context))
			{
				var result = db.GetTable<Person>()
					.Where(x => Sql.Property<string>(x.Patient, DiagnosisColumn) ==
								"Hallucination with Paranoid Bugs\' Delirium of Persecution")
					.ToList();

				Assert.AreEqual(1, result.Count);
				Assert.AreEqual("Tester", result.Single().FirstName);
			}
		}

		[Test]
		public void SqlPropertyWithNonDynamicAssociation([DataSources] string context)
		{
			using (var db = GetDataContext(context))
			{
				var result = db.GetTable<Person>()
					.Where(x => Sql.Property<string>(Sql.Property<Patient>(x, PatientColumn), DiagnosisColumn) ==
								"Hallucination with Paranoid Bugs\' Delirium of Persecution")
					.ToList();

				Assert.AreEqual(1, result.Count);
				Assert.AreEqual("Tester", result.Single().FirstName);
			}
		}

		[Test]
		public void SqlPropertyWithNonDynamicAssociationViaObject1([DataSources] string context)
		{
			using (var db = GetDataContext(context))
			{
				var result = db.GetTable<Person>()
					.Where(x => (string)Sql.Property<object>(Sql.Property<object>(x, PatientColumn), DiagnosisColumn) ==
								"Hallucination with Paranoid Bugs\' Delirium of Persecution")
					.ToList();

				Assert.AreEqual(1, result.Count);
				Assert.AreEqual("Tester", result.Single().FirstName);
			}
		}

		[Test]
		public void SqlPropertyWithNonDynamicAssociationViaObject2([DataSources] string context)
		{
			using (var db = GetDataContext(context))
			{
				var expected = Person.Select(p => p.Patient?.Diagnosis).ToList();
				var result = db.GetTable<Person>()
					.Select(x => Sql.Property<object>(Sql.Property<object>(x, PatientColumn), DiagnosisColumn))
					.ToList();

				Assert.IsTrue(result.OrderBy(_ => _ as string).SequenceEqual(expected.OrderBy(_ => _)));
			}
		}

		[Test]
		public void SqlPropertyWithDynamicColumn([DataSources] string context)
		{
			using (var db = GetDataContext(context, ConfigureDynamicClass()))
			{
				var result = db.GetTable<PersonWithDynamicStore>()
					.Where(x => Sql.Property<string>(x, "FirstName") == "John")
					.ToList();

				Assert.AreEqual(1, result.Count);
				Assert.AreEqual(1, result.Single().ID);
			}
		}

		[Test]
		public void SqlPropertyWithDynamicAssociation([DataSources] string context)
		{
			using (var db = GetDataContext(context, ConfigureDynamicClass()))
			{
				var result = db.GetTable<PersonWithDynamicStore>()
					.Where(x => Sql.Property<string>(Sql.Property<Patient>(x, PatientColumn), DiagnosisColumn) ==
								"Hallucination with Paranoid Bugs\' Delirium of Persecution")
					.ToList();

				Assert.AreEqual(1, result.Count);
				Assert.AreEqual(2, result.Single().ID);
			}
		}

		[Test]
		public void SqlPropertySelectAll([DataSources] string context)
		{
			using (var db = GetDataContext(context, ConfigureDynamicClass()))
			{
				var expected = Person.Select(p => p.FirstName).ToList();
				var result = db.GetTable<PersonWithDynamicStore>().ToList().Select(p => p.ExtendedProperties["FirstName"]).ToList();

				Assert.IsTrue(result.SequenceEqual(expected));
			}
		}

		[Test]
		public void SqlPropertySelectOne([DataSources] string context)
		{
			using (var db = GetDataContext(context, ConfigureDynamicClass()))
			{
				var expected = Person.Select(p => p.FirstName).ToList();
				var result = db.GetTable<PersonWithDynamicStore>()
					.Select(x => Sql.Property<string>(x, "FirstName"))
					.ToList();

				Assert.IsTrue(result.SequenceEqual(expected));
			}
		}

		[Test]
		public void SqlPropertySelectProject([DataSources] string context)
		{
			using (var db = GetDataContext(context, ConfigureDynamicClass()))
			{
				var expected = Person.Select(p => new
				{
					PersonId = p.ID,
					Name = p.FirstName
				}).ToList();

				var result = db.GetTable<PersonWithDynamicStore>()
					.Select(x => new
					{
						PersonId = x.ID,
						Name = Sql.Property<string>(x, "FirstName")
					})
					.ToList();

				Assert.IsTrue(result.SequenceEqual(expected));
			}
		}

		[Test]
		public void SqlPropertySelectAssociated([DataSources] string context)
		{
			using (var db = GetDataContext(context, ConfigureDynamicClass()))
			{
				var expected = Person.Select(p => p.Patient?.Diagnosis).ToList();

				var result = db.GetTable<PersonWithDynamicStore>()
					.Select(x => Sql.Property<string>(Sql.Property<Patient>(x, PatientColumn), DiagnosisColumn))
					.ToList();

				Assert.IsTrue(result.OrderBy(_ => _).SequenceEqual(expected.OrderBy(_ => _)));
			}
		}

		[Test]
		public void SqlPropertyWhere([DataSources] string context)
		{
			using (var db = GetDataContext(context, ConfigureDynamicClass()))
			{
				var expected = Person.Where(p => p.FirstName == "John").Select(p => p.ID).ToList();
				var result = db.GetTable<PersonWithDynamicStore>()
					.Where(x => Sql.Property<string>(x, "FirstName") == "John")
					.Select(x => x.ID)
					.ToList();

				Assert.IsTrue(result.SequenceEqual(expected));
			}
		}

		[Test]
		public void SqlPropertyWhereAssociated([DataSources] string context)
		{
			using (var db = GetDataContext(context, ConfigureDynamicClass()))
			{
				var expected = Person.Where(p => p.Patient?.Diagnosis != null).Select(p => p.ID).ToList();
				var result = db.GetTable<PersonWithDynamicStore>()
					.Where(x => Sql.Property<string>(Sql.Property<Patient>(x, PatientColumn), DiagnosisColumn) != null)
					.Select(x => x.ID)
					.ToList();

				Assert.IsTrue(result.SequenceEqual(expected));
			}
		}

		[Test]
		public void SqlPropertyOrderBy([DataSources] string context)
		{
			using (var db = GetDataContext(context, ConfigureDynamicClass()))
			{
				var expected = Person.OrderByDescending(p => p.FirstName).Select(p => p.ID).ToList();
				var result = db.GetTable<PersonWithDynamicStore>()
					.OrderByDescending(x => Sql.Property<string>(x, "FirstName"))
					.Select(x => x.ID)
					.ToList();

				Assert.IsTrue(result.SequenceEqual(expected));
			}
		}

		[Test]
		public void SqlPropertyOrderByAssociated([DataSources] string context)
		{
			using (var db = GetDataContext(context, ConfigureDynamicClass()))
			{
				var expected = Person.OrderBy(p => p.Patient?.Diagnosis).Select(p => p.ID).ToList();
				var result = db.GetTable<PersonWithDynamicStore>()
					.OrderBy(x => Sql.Property<string>(Sql.Property<Patient>(x, PatientColumn), DiagnosisColumn))
					.Select(x => x.ID)
					.ToList();

				Assert.IsTrue(result.OrderBy(_ => _).SequenceEqual(expected.OrderBy(_ => _)));
			}
		}

		[Test]
		public void SqlPropertyGroupBy([DataSources] string context)
		{
			using (var db = GetDataContext(context, ConfigureDynamicClass()))
			{
				var expected = Person.GroupBy(p => p.FirstName).Select(p => new {p.Key, Count = p.Count()}).ToList();
				var result = db.GetTable<PersonWithDynamicStore>()
					.GroupBy(x => Sql.Property<string>(x, "FirstName"))
					.Select(p => new {p.Key, Count = p.Count()})
					.ToList();

				Assert.IsTrue(result.OrderBy(_ => _.Key).SequenceEqual(expected.OrderBy(_ => _.Key)));
			}
		}

		[Test]
		public void SqlPropertyGroupByAssociated([DataSources] string context)
		{
			using (var db = GetDataContext(context, ConfigureDynamicClass()))
			{
				var expected = Person.GroupBy(p => p.Patient?.Diagnosis).Select(p => new {p.Key, Count = p.Count()}).ToList();
				var result   = db.GetTable<PersonWithDynamicStore>()
					.GroupBy(x => Sql.Property<string?>(Sql.Property<Patient>(x, PatientColumn), DiagnosisColumn))
					.Select(p => new { p.Key, Count = p.Count() })
					.ToList();

				Assert.IsTrue(result.OrderBy(_ => _.Key).SequenceEqual(expected.OrderBy(_ => _.Key)));
			}
		}

		[Test]
		public void SqlPropertyJoin([DataSources] string context)
		{
			using (var db = GetDataContext(context, ConfigureDynamicClass()))
			{
				var expected =
					from p in Person
					join pa in Patient on p.FirstName equals pa.Diagnosis
					select p;

				var result =
					from p in db.Person
					join pa in db.Patient on Sql.Property<string>(p, "FirstName") equals Sql.Property<string>(pa, DiagnosisColumn)
					select p;

				Assert.IsTrue(result.ToList().SequenceEqual(expected.ToList()));
			}
		}

		[Test]
		public void SqlPropertyLoadWith([DataSources] string context)
		{
			using (var db = GetDataContext(context, ConfigureDynamicClass()))
			{
				var expected = Person.Select(p => p.Patient?.Diagnosis).ToList();
				var result = db.GetTable<PersonWithDynamicStore>()
					.LoadWith(x => Sql.Property<Patient>(x, "Patient"))
					.ToList()
					.Select(p => ((Patient)p.ExtendedProperties[PatientColumn])?.Diagnosis)
					.ToList();

				Assert.IsTrue(result.OrderBy(_ => _).SequenceEqual(expected.OrderBy(_ => _)));
			}
		}

		[Test]
		public void SqlPropertyNoStoreGrouping1([DataSources] string context)
		{
			using (var db = GetDataContext(context, ConfigureDynamicClass()))
			{
				var expected =
					from p in Person
					group p by p.FirstName into g
					select new
					{
						FirstName = g.Key,
						Count = g.Count()
					};

				var result =
					from p in db.GetTable<PersonWithDynamicStore>()
					group p by Sql.Property<string>(p, "FirstName") into g
					select new
					{
						FirstName = g.Key,
						Count = g.Count()
					};

				AreEqual(result.OrderBy(_ => _.FirstName), expected.OrderBy(_ => _.FirstName));
			}
		}

		[Test]
		public void SqlPropertyNoStoreGrouping2([DataSources] string context)
		{
			using (var db = GetDataContext(context))
			{
				var expected =
					from p in Person
					group p by new { p.FirstName, p.LastName } into g
					select new
					{
						g.Key.FirstName,
						g.Key.LastName,
						Count = g.Count()
					};

				var result =
					from p in db.GetTable<PersonWithoutDynamicStore>()
					group p by new { FirstName = Sql.Property<string>(p, "FirstName"), LastName = Sql.Property<string>(p, "LastName")} into g
					select new
					{
						g.Key.FirstName,
						g.Key.LastName,
						Count = g.Count()
					};

				AreEqual(result.OrderBy(_ => _.FirstName), expected.OrderBy(_ => _.FirstName));
			}
		}

		[Test]
		public void SqlPropertyNoStoreNonIdentifier([DataSources] string context)
		{
			using (new FirebirdQuoteMode(FirebirdIdentifierQuoteMode.Auto))
			using (var db = GetDataContext(context))
			using (db.CreateLocalTable(new []
			{
				new DynamicTablePrototype { NotIdentifier = 77 }
			}))
			{
				var query =
					from d in db.GetTable<DynamicTable>()
					select new
					{
						NI = Sql.Property<int>(d, "Not Identifier")
					};

				var result = query.ToArray();

				Assert.AreEqual(77, result[0].NI);
			}
		}

		[Test]
		public void SqlPropertyNoStoreNonIdentifierGrouping([DataSources] string context)
		{
			using (new FirebirdQuoteMode(FirebirdIdentifierQuoteMode.Auto))
			using (var db = GetDataContext(context))
			using (db.CreateLocalTable(new []
			{
				new DynamicTablePrototype { NotIdentifier = 77, Value = 5 },
				new DynamicTablePrototype { NotIdentifier = 77, Value = 5 }
			}))
			{
				var query =
					from d in db.GetTable<DynamicTable>()
					group d by new { NI = Sql.Property<int>(d, "Not Identifier") }
					into g
					select new
					{
						g.Key.NI,
						Count = g.Count(),
						Sum = g.Sum(i => Sql.Property<int>(i, "Some Value"))
					};

				var result = query.ToArray();

				Assert.AreEqual(77, result[0].NI);
				Assert.AreEqual(2,  result[0].Count);
				Assert.AreEqual(10, result[0].Sum);
			}
		}

		private MappingSchema ConfigureDynamicClass()
		{
			var ms = new MappingSchema();

			new FluentMappingBuilder(ms)
				.Entity<PersonWithDynamicStore>().HasTableName("Person")
				.HasPrimaryKey(x => Sql.Property<int>(x, "ID"))
				.Property(x => Sql.Property<string>(x, "FirstName")).IsNullable(false)
				.Property(x => Sql.Property<string>(x, "LastName")).IsNullable(false)
				.Property(x => Sql.Property<string>(x, "MiddleName"))
				.Association(x => Sql.Property<Patient>(x, "Patient"), x => Sql.Property<int>(x, "ID"), x => x.PersonID)
				.Build();

			return ms;
		}

		public class PersonWithDynamicStore
		{
			[Column("PersonID"), Identity]
			public int ID { get; set; }

			[DynamicColumnsStore]
			public IDictionary<string, object> ExtendedProperties { get; set; } = null!;
		}

		[Table("Person")]
		public class PersonWithoutDynamicStore
		{
			[Column("PersonID"), Identity, PrimaryKey]
			public int ID { get; set; }
		}

		[Table("DynamicTable")]
		public class DynamicTablePrototype
		{
			[Column, Identity, PrimaryKey]
			public int ID { get; set; }

			[Column("Not Identifier")]
			public int NotIdentifier { get; set; }

			[Column("Some Value")]
			public int Value { get; set; }
		}

		[Table("DynamicTable")]
		public class DynamicTable
		{
			[Column, Identity, PrimaryKey]
			public int ID { get; set; }
		}

		public class SomeClassWithDynamic
		{
			public string? Description { get; set; }

			private sealed class SomeClassEqualityComparer : IEqualityComparer<SomeClassWithDynamic>
			{
				public bool Equals(SomeClassWithDynamic? x, SomeClassWithDynamic? y)
				{
					if (ReferenceEquals(x, y))      return true;
					if (ReferenceEquals(x, null))   return false;
					if (ReferenceEquals(y, null))   return false;
					if (x.GetType() != y.GetType()) return false;
					if (!string.Equals(x.Description, y.Description))
						return false;

					if (x.ExtendedProperties == null && y.ExtendedProperties == null)
						return true;

					if (x.ExtendedProperties == null || y.ExtendedProperties == null)
						return false;

					bool CompareValues(IDictionary<string, object> values1, IDictionary<string, object> values2)
					{
						foreach (var property in values1)
						{
							var value1 = property.Value as string ?? string.Empty;
							values2.TryGetValue(property.Key, out var value);
							var value2 = value as string ?? string.Empty;
							if (!string.Equals(value1, value2))
								return false;
						}

						return true;
					}

					return CompareValues(x.ExtendedProperties, y.ExtendedProperties) &&
					       CompareValues(y.ExtendedProperties, x.ExtendedProperties);
				}

				public int GetHashCode(SomeClassWithDynamic obj)
				{
					return (obj.Description != null ? obj.Description.GetHashCode() : 0);
				}
			}

			public static IEqualityComparer<SomeClassWithDynamic> SomeClassComparer { get; } = new SomeClassEqualityComparer();

			[DynamicColumnsStore]
			public IDictionary<string, object> ExtendedProperties { get; set; } = null!;
		}

		[Test]
		public void TestConcatWithDynamic([IncludeDataSources(true, TestProvName.AllSQLiteClassic, TestProvName.AllClickHouse)] string context)
		{
			var mappingSchema = new MappingSchema();
			var builder = new FluentMappingBuilder(mappingSchema)
				.Entity<SomeClassWithDynamic>();

			builder.Property(x => x.Description).HasColumnName("F066_04");
			builder.Property(x => Sql.Property<string>(x, "F066_05"));
			builder.Property(x => Sql.Property<string>(x, "F066_00"));

			builder.Build();

			var testData1 = new[]
			{
				new SomeClassWithDynamic{Description = "Desc1", ExtendedProperties = new Dictionary<string, object>{{"F066_05", "v1"}}},
				new SomeClassWithDynamic{Description = "Desc2", ExtendedProperties = new Dictionary<string, object>{{"F066_05", "v2"}}},
			};

			var testData2 = new[]
			{
				new SomeClassWithDynamic{Description = "Desc3", ExtendedProperties = new Dictionary<string, object>{{"F066_00", "v3"}}},
				new SomeClassWithDynamic{Description = "Desc4", ExtendedProperties = new Dictionary<string, object>{{"F066_00", "v4"}}},
			};

			using (var dataContext = GetDataContext(context, mappingSchema))
			{
				using (dataContext.CreateLocalTable("M998_T066", testData1))
				using (dataContext.CreateLocalTable("M998_T000", testData2))
				{
					var expected = testData1.Concat(testData2);
					var result =
						dataContext.GetTable<SomeClassWithDynamic>().TableName("M998_T066")
							.Concat(dataContext.GetTable<SomeClassWithDynamic>().TableName("M998_T000"))
							.ToList();

					AreEqual(expected, result, SomeClassWithDynamic.SomeClassComparer);
				}
			}
		}

		sealed class BananaTable
		{
			public int Id { get; set; }
			public string? Property { get; set; }
		}

		[Test(Description = "https://stackoverflow.com/questions/61081571: Expression 't.Id' is not a Field.")]
		public void DynamicGoesBanana1([IncludeDataSources(true, TestProvName.AllSQLite)] string context)
		{
			using (var db = GetDataContext(context))
			using (db.CreateLocalTable<BananaTable>())
			{
				db.GetTable<BananaTable>().Insert(() => new BananaTable() { Id = 1, Property = "test1" });

				var res = db.GetTable<BananaTable>().ToList();
				Assert.AreEqual(1, res.Count);
				Assert.AreEqual("test1", res[0].Property);

				Test(nameof(BananaTable), nameof(BananaTable.Id), nameof(BananaTable.Property), 1, "banana");

				res = db.GetTable<BananaTable>().ToList();
				Assert.AreEqual(1, res.Count);
				Assert.AreEqual("banana", res[0].Property);

				void Test(string entity, string filterProperty, string changedProperty, object filter, object value)
				{
					db.GetTable<object>()
						.TableName(entity)
						.Where(t => Sql.Property<object>(t, filterProperty).Equals(filter))
						.Set(t => Sql.Property<object>(t, changedProperty), value)
						.Update();
				}
			}
		}

		[Test]
		public void DynamicGoesBanana2([IncludeDataSources(true, TestProvName.AllSQLiteClassic, TestProvName.AllClickHouse)] string context)
		{
			using (var db = GetDataContext(context))
			using (db.CreateLocalTable<BananaTable>())
			{
				db.GetTable<BananaTable>().Insert(() => new BananaTable() { Id = 1, Property = "test1" });

				var res = db.GetTable<BananaTable>().ToList();
				Assert.AreEqual(1, res.Count);
				Assert.AreEqual("test1", res[0].Property);

				Test<BananaTable>(nameof(BananaTable), nameof(BananaTable.Id), nameof(BananaTable.Property), 1, "banana");

				res = db.GetTable<BananaTable>().ToList();
				Assert.AreEqual(1, res.Count);
				Assert.AreEqual("banana", res[0].Property);

				void Test<TEntity>(string entity, string filterProperty, string changedProperty, object filter, object value)
					where TEntity : class
				{
					db.GetTable<TEntity>()
						.TableName(entity)
						.Where(t => Sql.Property<TEntity>(t, filterProperty)!.Equals(filter))
						.Set(t => Sql.Property<TEntity>(t, changedProperty)!, value)
						.Update();
				}
			}
		}

		[Test]
		public void Issue3158([DataSources(TestProvName.AllClickHouse)] string context)
		{
			using (var db = GetDataContext(context))
			{
				Assert.DoesNotThrow(() => {
					(from p in db.GetTable<PersonWithoutDynamicStore>()
					 join d in db.Doctor on p.ID equals d.PersonID
					 from pa in db.Patient.LeftJoin(pa => pa.Diagnosis == Sql.Property<string>(p, "FirstName"))
					 select new { p.ID, pa.Diagnosis })
					.ToList();
				});
			}
		}
	}
}
