/* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
/*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*
*  Licensed to Elasticsearch B.V. under one or more contributor
*  license agreements. See the NOTICE file distributed with
*  this work for additional information regarding copyright
*  ownership. Elasticsearch B.V. 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
*
* 	http://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.Collections.Generic;
using System.Linq;
using System.Text;
using OpenSearch.Net;
using FluentAssertions;
using OpenSearch.Client;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using Tests.Core.Client;
using Tests.Core.Extensions;

namespace Tests.Core.Serialization
{
	public class SerializationResult
	{
		public string DiffFromExpected { get; set; }

		public string Serialized { get; set; }
		public bool Success { get; set; }

		private string DiffFromExpectedExcerpt =>
			string.IsNullOrEmpty(DiffFromExpected)
				? string.Empty
				: DiffFromExpected?
//					.Replace("{", "{{") // escape for string format in FluentAssertion
//					.Replace("}", "}}")
					.Substring(0, DiffFromExpected.Length > 4896 ? 4896 : DiffFromExpected.Length);

		public override string ToString()
		{
			var message = $"{GetType().Name} success: {Success}";
			if (Success) return message;

			message += Environment.NewLine;
			message += DiffFromExpectedExcerpt;
			return message;
		}
	}

	public class DeserializationResult<T> : SerializationResult
	{
		public T Result { get; set; }

		public override string ToString()
		{
			var s = $"Deserialization has result: {Result != null}";
			s += Environment.NewLine;
			s += base.ToString();
			return s;
		}
	}

	public class RoundTripResult<T> : DeserializationResult<T>
	{
		public int Iterations { get; set; }

		public override string ToString()
		{
			var s = $"RoundTrip: {Iterations.ToOrdinal()} iteration";
			s += Environment.NewLine;
			s += base.ToString();
			return s;
		}
	}

	public class SerializationTester
	{
		public SerializationTester(IOpenSearchClient client) => Client = client;

		public IOpenSearchClient Client { get; }
		public static SerializationTester Default { get; } = new SerializationTester(TestClient.DefaultInMemoryClient);

		public static SerializationTester DefaultWithJsonNetSerializer { get; } = new SerializationTester(TestClient.InMemoryWithJsonNetSerializer);

		protected IOpenSearchSerializer Serializer => Client.ConnectionSettings.RequestResponseSerializer;

		public RoundTripResult<T> RoundTrips<T>(T @object, bool preserveNullInExpected = false)
		{
			var serialized = SerializeUsingClientDefault(@object);
			return RoundTrips(@object, serialized);
		}

		public RoundTripResult<T> RoundTrips<T>(T @object, object expectedJson, bool preserveNullInExpected = false)
		{
			var expectedJsonToken = ExpectedJsonToJtoken(expectedJson, preserveNullInExpected);

			var result = new RoundTripResult<T>() { Success = false };
			if (expectedJsonToken == null)
			{
				result.DiffFromExpected = "Expected json was null";
				return result;
			}

			if (!SerializesAndMatches(@object, expectedJsonToken, result))
				return result;

			@object = Deserialize<T>(result.Serialized);
			result.Iterations += 1;

			if (!SerializesAndMatches(@object, expectedJsonToken, result))
				return result;

			@object = Deserialize<T>(result.Serialized);

			result.Result = @object;
			result.Success = true;
			return result;
		}

		public SerializationResult Serializes<T>(T @object, object expectedJson, bool preserveNullInExpected = false)
		{
			var expectedJsonToken = ExpectedJsonToJtoken(expectedJson, preserveNullInExpected);
			var result = new RoundTripResult<T>() { Success = false };
			if (SerializesAndMatches(@object, expectedJsonToken, result))
				result.Success = true;
			return result;
		}

		public DeserializationResult<T> Deserializes<T>(object expectedJson, bool preserveNullInExpected = false)
		{
			var expectedJsonString = ExpectedJsonString(expectedJson, preserveNullInExpected);
			var result = new RoundTripResult<T>() { Success = false };
			var @object = Deserialize<T>(expectedJsonString);
			if (@object != null) result.Success = true;
			result.Result = @object;
			return result;
		}

		private JToken ExpectedJsonToJtoken(object expectedJson, bool preserveNullInFromJson)
		{
			switch (expectedJson)
			{
				case string s: return new JValue(s);
				case byte[] utf8: return new JValue(Encoding.UTF8.GetString(utf8));
				default:
					var expectedJsonString = ExpectedJsonString(expectedJson, preserveNullInFromJson);
					return JToken.Parse(expectedJsonString);
			}
		}

		private string ExpectedJsonString(object expectedJson, bool preserveNullInFromJson)
		{
			switch (expectedJson)
			{
				case string s: return s;
				case byte[] utf8: return Encoding.UTF8.GetString(utf8);
				default:
					var expectedSerializerSettings = ExpectedJsonSerializerSettings(preserveNullInFromJson);
					return JsonConvert.SerializeObject(expectedJson, Formatting.None, expectedSerializerSettings);
			}
		}

		private bool SerializesAndMatches<T>(T @object, JToken expectedJsonToken, RoundTripResult<T> result)
		{
			result.Serialized = SerializeUsingClientDefault(@object);
			return expectedJsonToken.Type == JTokenType.Array
				? ArrayMatches((JArray)expectedJsonToken, result)
				: TokenMatches(expectedJsonToken, result);
		}

		private string SerializeUsingClientDefault<T>(T o)
		{
			switch (o)
			{
				case string s: return s;
				case byte[] b: return Encoding.UTF8.GetString(b);
				default:
					return Serializer.SerializeToString(o);
			}
		}

		private T Deserialize<T>(string json)
		{
			using (var ms = Client.ConnectionSettings.MemoryStreamFactory.Create(Encoding.UTF8.GetBytes(json)))
				return Serializer.Deserialize<T>(ms);
		}

		private static bool ArrayMatches<T>(JArray jArray, RoundTripResult<T> result)
		{
			var lines = result.Serialized.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries).ToList();
			var zipped = jArray.Children<JObject>().Zip(lines, (j, s) => new { j, s });
			var matches = zipped.Select((z, i) => TokenMatches(z.j, result, z.s, i)).ToList();
			matches.Count.Should().Be(lines.Count);
			var matchesAll = matches.All(b => b);
			if (matchesAll) return true;

			matches.Should().OnlyContain(b => b, "{0}", result.DiffFromExpected);
			return matches.All(b => b);
		}

		private static bool TokenMatches<T>(JToken expectedJson, RoundTripResult<T> result, string itemJson = null, int item = -1)
		{
			var actualJson = itemJson ?? result.Serialized;
			var message = "This is the first time I am serializing";
			if (result.Iterations > 0)
				message = "This is the second time I am serializing, this usually indicates a problem when deserializing";
			if (item > -1) message += $". This is while comparing the {item.ToOrdinal()} item";

			if (expectedJson.Type == JTokenType.String) return MatchString(expectedJson.Value<string>(), actualJson, result, message);

			return MatchJson(expectedJson, actualJson, result, message);
		}

		private static bool MatchString<T>(string expected, string actual, RoundTripResult<T> result, string message)
		{
			//Serialize() returns quoted strings always.
			var diff = expected.CreateCharacterDifference(actual, message);
			if (string.IsNullOrWhiteSpace(diff)) return true;

			result.DiffFromExpected = diff;
			return false;
		}

		private static bool MatchJson<T>(JToken expectedJson, string actualJson, RoundTripResult<T> result, string message)
		{
			JToken actualJsonToken = null;
			try
			{
				actualJsonToken = JToken.Parse(actualJson);
			}
			catch (Exception e)
			{
				throw new Exception($"Invalid json: {actualJson}", e);
			}
			var matches = JToken.DeepEquals(expectedJson, actualJsonToken);
			if (matches) return true;

			(actualJsonToken as JObject)?.DeepSort();
			(expectedJson as JObject)?.DeepSort();

			var sortedExpected = expectedJson.ToString();
			var sortedActual = actualJsonToken.ToString();
			var diff = sortedExpected.Diff(sortedActual, message);
			if (string.IsNullOrWhiteSpace(diff)) return true;

			result.DiffFromExpected = diff;
			return false;
		}

		private JsonSerializerSettings ExpectedJsonSerializerSettings(bool preserveNullInExpected = false) =>
			new JsonSerializerSettings
			{
				ContractResolver = new DefaultContractResolver { NamingStrategy = new DefaultNamingStrategy() },
				NullValueHandling = preserveNullInExpected ? NullValueHandling.Include : NullValueHandling.Ignore,
				//copied here because anonymyzing geocoordinates is too tedious
				Converters = new List<JsonConverter> { new TestGeoCoordinateJsonConverter() }
			};
	}
}