using NUnit.Framework;
using System;
using System.Linq;
using System.Runtime.InteropServices;
using Vanara.InteropServices;
using Vanara.PInvoke;

namespace Vanara.Extensions.Tests
{
	[TestFixture()]
	public class InteropExtensionsTests
	{
		private readonly int i = Marshal.SizeOf(typeof(int));

		[Test()]
		public void IsBlittableTest()
		{
			Assert.That(typeof(int).IsBlittable(), Is.True);
			Assert.That(typeof(Guid).IsBlittable(), Is.True);
			Assert.That(typeof(RECT).IsBlittable(), Is.True);
			Assert.That(typeof(TypeCode).IsBlittable(), Is.True);
			Assert.That(typeof(PRECT).IsBlittable(), Is.True);
			Assert.That(typeof(int[]).IsBlittable(), Is.True);
			Assert.That(typeof(RECT[]).IsBlittable(), Is.True);
			Assert.That(typeof(decimal).IsBlittable(), Is.True);
			Assert.That(typeof(bool).IsBlittable(), Is.True);
			Assert.That(typeof(DateTime).IsBlittable(), Is.False);
			Assert.That(typeof(string).IsBlittable(), Is.False);
			Assert.That(typeof(string[]).IsBlittable(), Is.False);
			Assert.That(typeof(AbsClass).IsBlittable(), Is.False);
			Assert.That(typeof(IDisposable).IsBlittable(), Is.False);
			//Assert.That(typeof(int[][]).IsBlittable(), Is.False);
			Assert.That(typeof(RECT?).IsBlittable(), Is.False);
		}

		[Test()]
		public void IsNullableTest()
		{
			Assert.That(typeof(int).IsNullable(), Is.False);
			Assert.That(typeof(RECT?).IsNullable(), Is.True);
			Assert.That(typeof(int?).IsNullable(), Is.True);
		}

		[Test()]
		public void MarshalToPtrTest()
		{
			var h = new SafeHGlobalHandle(Marshal.SizeOf(typeof(RECT)) * 2 + i);
			var rs = new[] { new RECT(), new RECT(10, 11, 12, 13) };
			((IntPtr)h).Write(rs, i, h.Size);
			Assert.That(Marshal.ReadInt32((IntPtr)h, 4 * i) == 0);
			Assert.That(Marshal.ReadInt32((IntPtr)h, 5 * i) == 10);
			Assert.That(Marshal.ReadInt32((IntPtr)h, 7 * i) == 12);
			var ro = ((IntPtr)h).ToArray<RECT>(2, i);
			Assert.That(ro.Length == 2);
			Assert.That(ro[0].left == 0);
			Assert.That(ro[1].right == 12);
		}

		[Test()]
		public void MarshalToPtrTest1()
		{
			var h = IntPtr.Zero;
			try
			{
				var rs = new[] { new RECT(), new RECT(10, 11, 12, 13) };
				h = rs.MarshalToPtr(Marshal.AllocHGlobal, out var a, i);
				Assert.That(h, Is.Not.EqualTo(IntPtr.Zero));
				Assert.That(a, Is.EqualTo(Marshal.SizeOf(typeof(RECT)) * rs.Length + i));
				Assert.That(Marshal.ReadInt32(h, 4 * i) == 0);
				Assert.That(Marshal.ReadInt32(h, 5 * i) == 10);
				Assert.That(Marshal.ReadInt32(h, 7 * i) == 12);
				var ro = h.ToArray<RECT>(rs.Length, i);
				Assert.That(ro.Length == 2);
				Assert.That(ro[0].left == 0);
				Assert.That(ro[1].right == 12);
				Marshal.FreeHGlobal(h);

				h = new RECT[0].MarshalToPtr(Marshal.AllocHGlobal, out a, i);
				Assert.That(h, Is.Not.EqualTo(IntPtr.Zero));
				Assert.That(a, Is.EqualTo(i));

				Assert.That(() => new DateTime[1].MarshalToPtr(Marshal.AllocHGlobal, out a, i), Throws.Exception);
			}
			finally
			{
				Marshal.FreeHGlobal(h);
			}
		}

		[Test()]
		public void MarshalToPtrTest2()
		{
			var h = IntPtr.Zero;
			try
			{
				var rs = new[] { "str1", "str2", "str3" };
				h = rs.MarshalToPtr(StringListPackMethod.Concatenated, Marshal.AllocHGlobal, out var a, CharSet.Unicode, i);
				Assert.That(h, Is.Not.EqualTo(IntPtr.Zero));
				var chSz = 2;
				Assert.That(a, Is.EqualTo(chSz * (rs[0].Length + 1) * rs.Length + chSz + i));
				var ro = h.ToArray<byte>(a - i, i);
				var chars = System.Text.Encoding.Unicode.GetChars(ro);
				Assert.That(chars.Length, Is.EqualTo((a - i) / chSz));
				Assert.That(chars[0], Is.EqualTo('s'));
				Assert.That(chars[4], Is.EqualTo('\0'));
				Assert.That(chars[chars.Length - 2], Is.EqualTo('\0'));
				Assert.That(chars[chars.Length - 1], Is.EqualTo('\0'));
				Marshal.FreeHGlobal(h);

				h = rs.MarshalToPtr(StringListPackMethod.Concatenated, Marshal.AllocHGlobal, out a, CharSet.Ansi, i);
				Assert.That(h, Is.Not.EqualTo(IntPtr.Zero));
				chSz = 1;
				Assert.That(a, Is.EqualTo(chSz * (rs[0].Length + 1) * rs.Length + chSz + i));
				ro = h.ToArray<byte>(a - i, i);
				chars = System.Text.Encoding.ASCII.GetChars(ro);
				Assert.That(chars.Length, Is.EqualTo((a - i) / chSz));
				Assert.That(chars[0], Is.EqualTo('s'));
				Assert.That(chars[4], Is.EqualTo('\0'));
				Assert.That(chars[chars.Length - 2], Is.EqualTo('\0'));
				Assert.That(chars[chars.Length - 1], Is.EqualTo('\0'));
				Marshal.FreeHGlobal(h);

				h = new string[0].MarshalToPtr(StringListPackMethod.Concatenated, Marshal.AllocHGlobal, out a, CharSet.Unicode, i);
				Assert.That(h, Is.Not.EqualTo(IntPtr.Zero));
				Assert.That(a, Is.EqualTo(i + 2));

				Assert.That(() => new[] { "" }.MarshalToPtr(StringListPackMethod.Concatenated, Marshal.AllocHGlobal, out a, CharSet.Unicode, i), Throws.ArgumentException);
				Assert.That(() => new string[] { null }.MarshalToPtr(StringListPackMethod.Concatenated, Marshal.AllocHGlobal, out a, CharSet.Unicode, i), Throws.ArgumentException);
			}
			finally
			{
				Marshal.FreeHGlobal(h);
			}
		}

		[Test()]
		public void MarshalToPtrTest3()
		{
			var h = IntPtr.Zero;
			try
			{
				var rs = new[] { "str1", "str2", "str3" };
				h = rs.MarshalToPtr(StringListPackMethod.Packed, Marshal.AllocHGlobal, out var a, CharSet.Unicode, i);
				Assert.That(h, Is.Not.EqualTo(IntPtr.Zero));
				var chSz = 2;
				Assert.That(a, Is.EqualTo(chSz * (rs[0].Length + 1) * rs.Length + ((rs.Length + 1) * IntPtr.Size) + i));
				var ro = h.ToIEnum<IntPtr>(rs.Length, i).ToArray();
				Assert.That(ro, Has.None.EqualTo(IntPtr.Zero));
				for (var i = 0; i < ro.Length; i++)
					Assert.That(StringHelper.GetString(ro[i], CharSet.Unicode), Is.EqualTo(rs[i]));
				Marshal.FreeHGlobal(h);

				h = rs.MarshalToPtr(StringListPackMethod.Packed, Marshal.AllocHGlobal, out a, CharSet.Ansi, i);
				Assert.That(h, Is.Not.EqualTo(IntPtr.Zero));
				chSz = 1;
				Assert.That(a, Is.EqualTo(chSz * (rs[0].Length + 1) * rs.Length + ((rs.Length + 1) * IntPtr.Size) + i));
				ro = h.ToIEnum<IntPtr>(rs.Length, i).ToArray();
				Assert.That(ro, Has.None.EqualTo(IntPtr.Zero));
				for (var i = 0; i < ro.Length; i++)
					Assert.That(StringHelper.GetString(ro[i], CharSet.Ansi), Is.EqualTo(rs[i]));
				Marshal.FreeHGlobal(h);

				h = new string[0].MarshalToPtr(StringListPackMethod.Packed, Marshal.AllocHGlobal, out a, CharSet.Unicode, i);
				Assert.That(h, Is.Not.EqualTo(IntPtr.Zero));
				Assert.That(a, Is.EqualTo(i + IntPtr.Size));
			}
			finally
			{
				Marshal.FreeHGlobal(h);
			}
		}

		[Test()]
		public void StructureToPtrTest()
		{
			var rect = new RECT(10, 11, 12, 13);
			var ptr = rect.MarshalToPtr(Marshal.AllocCoTaskMem, out var a);
			Assert.That(ptr != IntPtr.Zero);
			Assert.That(Marshal.ReadInt32(ptr, 1 * i) == 11);
			Marshal.FreeCoTaskMem(ptr);
		}

		[Test()]
		public void ToArrayTest()
		{
			var rs = new[] { 10, 11, 12, 13, 14 };
			var h = SafeHGlobalHandle.CreateFromList(rs, rs.Length, i);
			var ro = ((IntPtr)h).ToArray<int>(4, i);
			Assert.That(ro.Length, Is.EqualTo(4));
			Assert.That(ro[2], Is.EqualTo(rs[2]));

			Assert.That(((IntPtr)h).ToArray<int>(0, i), Is.Empty);
			Assert.That(IntPtr.Zero.ToArray<int>(3, i), Is.Null);
		}

		[Test()]
		public void ToIEnumTest()
		{
			var rs = new[] { 10, 11, 12, 13, 14 };
			var h = SafeHGlobalHandle.CreateFromList(rs, rs.Length, i);
			var ro = ((IntPtr)h).ToIEnum<int>(4, i);
			var v = 10;
			foreach (var rv in ro)
				Assert.That(rv, Is.EqualTo(v++));
			Assert.That(v, Is.EqualTo(14));

			Assert.That(((IntPtr)h).ToIEnum<int>(0, i), Is.Empty);
			Assert.That(IntPtr.Zero.ToIEnum<int>(3, i), Is.Empty);
		}

		[Test()]
		public void ConvertPtrTest()
		{
			Assert.That(new UIntPtr(0x0E924356).ToIntPtr(), Is.EqualTo(new IntPtr(0x0E924356)));
			Assert.That(new IntPtr(0x0E924356).ToUIntPtr(), Is.EqualTo(new UIntPtr(0x0E924356)));
		}

		[Test()]
		public void ToNullableStructureTest()
		{
			Assert.That(IntPtr.Zero.ToNullableStructure<RECT>(), Is.Null);
			var h = SafeHGlobalHandle.CreateFromStructure(new RECT(10, 11, 12, 13));
			Assert.That(((IntPtr)h).ToNullableStructure<RECT>(), Is.Not.Null);
		}

		[Test()]
		public void ToStructureTest()
		{
			var h = SafeHGlobalHandle.CreateFromStructure(new RECT(10, 11, 12, 13));
			Assert.That(((IntPtr)h).ToStructure<RECT>().left == 10);
			h.Dispose();
			Assert.That(() => ((IntPtr)h).ToStructure<RECT>(), Throws.TypeOf<NullReferenceException>());
		}

		[Test()]
		public void ToStructureTest1()
		{
			using var h = SafeHGlobalHandle.CreateFromStructure(new PRECT(10, 11, 12, 13));
			var r = new PRECT(0, 0, 0, 0);
			Assert.That(() => ((IntPtr)h).ToStructure(r), Throws.Nothing);
			Assert.That(r.left, Is.EqualTo(10));
		}

		private abstract class AbsClass
		{
			public abstract int Value { get; }
		}

		[Test]
		public void GetNulledPtrArrayLengthTest()
		{
			var ptrs = new[] { (IntPtr)1, (IntPtr)1, (IntPtr)1, (IntPtr)1, (IntPtr)1, IntPtr.Zero };
			var mp = ptrs.MarshalToPtr<IntPtr>(Marshal.AllocCoTaskMem, out _);
			Assert.That(mp.GetNulledPtrArrayLength(), Is.EqualTo(ptrs.Length - 1));
			Marshal.WriteIntPtr(mp, IntPtr.Size, IntPtr.Zero);
			Assert.That(mp.GetNulledPtrArrayLength(), Is.EqualTo(1));
			Marshal.WriteIntPtr(mp, IntPtr.Zero);
			Assert.That(mp.GetNulledPtrArrayLength(), Is.EqualTo(0));
			Assert.That(IntPtr.Zero.GetNulledPtrArrayLength(), Is.EqualTo(0));
			Marshal.FreeCoTaskMem(mp);
		}

		[Test()]
		public void ToStringEnumTest()
		{
			var rs = new[] { "str1", "str2", "str3" };
			using (var sa = SafeHGlobalHandle.CreateFromStringList(rs, StringListPackMethod.Concatenated, CharSet.Ansi, i))
			{
				var ptr = sa.DangerousGetHandle();
				Assert.That(ptr, Is.Not.EqualTo(IntPtr.Zero));
				var se = ptr.ToStringEnum(CharSet.Ansi, i, sa.Size);
				Assert.That(se, Is.EquivalentTo(rs));
				Assert.That(() => ptr.ToStringEnum(CharSet.Ansi, i, sa.Size - 5).ToArray(), Throws.TypeOf<InsufficientMemoryException>());
				Assert.That(() => ptr.ToStringEnum(CharSet.Ansi, i, sa.Size - 1).ToArray(), Throws.TypeOf<InsufficientMemoryException>());
			}
			using (var sa = SafeHGlobalHandle.CreateFromStringList(rs, StringListPackMethod.Concatenated, CharSet.Unicode, i))
			{
				var ptr = sa.DangerousGetHandle();
				Assert.That(ptr, Is.Not.EqualTo(IntPtr.Zero));
				var se = ptr.ToStringEnum(CharSet.Unicode, i, sa.Size);
				Assert.That(se, Is.EquivalentTo(rs));
				Assert.That(() => ptr.ToStringEnum(CharSet.Unicode, i, sa.Size - 5).ToArray(), Throws.TypeOf<InsufficientMemoryException>());
				Assert.That(() => ptr.ToStringEnum(CharSet.Unicode, i, sa.Size - 1).ToArray(), Throws.TypeOf<InsufficientMemoryException>());
			}
			using (var sa = SafeHGlobalHandle.CreateFromStringList(null, StringListPackMethod.Concatenated, CharSet.Unicode, i))
			{
				var ptr = sa.DangerousGetHandle();
				Assert.That(ptr, Is.Not.EqualTo(IntPtr.Zero));
				var se = ptr.ToStringEnum(CharSet.Unicode, i, sa.Size);
				Assert.That(se, Is.Empty);
				Assert.That(() => ptr.ToStringEnum(CharSet.Unicode, i, i).ToArray(), Throws.TypeOf<InsufficientMemoryException>());
				Assert.That(() => ptr.ToStringEnum(CharSet.Unicode, i, sa.Size - 1).ToArray(), Throws.TypeOf<InsufficientMemoryException>());
			}
			Assert.That(IntPtr.Zero.ToStringEnum(CharSet.Unicode, i), Is.Empty);
		}

		[Test()]
		public void ToStringEnumTest1()
		{
			var rs = new[] { "str1", "str2", null, "", "str3" };
			using (var sa = SafeHGlobalHandle.CreateFromStringList(rs, StringListPackMethod.Packed, CharSet.Ansi, i))
			{
				var ptr = sa.DangerousGetHandle();
				Assert.That(ptr, Is.Not.EqualTo(IntPtr.Zero));
				var se = ptr.ToStringEnum(rs.Length, CharSet.Ansi, i);
				Assert.That(se, Is.EquivalentTo(rs));
			}
			using (var sa = SafeHGlobalHandle.CreateFromStringList(rs, StringListPackMethod.Packed, CharSet.Unicode, i))
			{
				var ptr = sa.DangerousGetHandle();
				Assert.That(ptr, Is.Not.EqualTo(IntPtr.Zero));
				var se = ptr.ToStringEnum(rs.Length, CharSet.Unicode, i);
				Assert.That(se, Is.EquivalentTo(rs));
			}
			using (var sa = SafeHGlobalHandle.CreateFromStringList(null, StringListPackMethod.Packed, CharSet.Unicode, i))
			{
				var ptr = sa.DangerousGetHandle();
				Assert.That(ptr, Is.Not.EqualTo(IntPtr.Zero));
				var se = ptr.ToStringEnum(0, CharSet.Unicode, i);
				Assert.That(se, Is.Empty);
			}
			Assert.That(IntPtr.Zero.ToStringEnum(0, CharSet.Unicode, i), Is.Empty);
		}

		[TestCase("Some string value")]
		[TestCase("")]
		[TestCase((string)null)]
		public void ToInsecureStringTest(string sval)
		{
			var ss = sval.ToSecureString();
			if (sval != null)
			{
				Assert.That(ss, Is.Not.Null);
				Assert.That(ss.Length, Is.EqualTo(sval.Length));
				var s = ss.ToInsecureString();
				Assert.That(s, Is.EqualTo(sval));
			}
			else
			{
				Assert.That(ss, Is.Null);
			}
		}

		[Test]
		public void ToInsecureStringTest()
		{
			Assert.That(IntPtr.Zero.ToSecureString(4), Is.Null);
			Assert.That(((System.Security.SecureString)null).ToInsecureString(), Is.Null);
		}

		[TestCase("Some string value")]
		[TestCase("")]
		[TestCase((string)null)]
		public void ToSecureStringTest(string sval)
		{
			var ms = new SafeCoTaskMemString(sval);
			var ss = ms.DangerousGetHandle().ToSecureString();
			if (sval != null)
			{
				Assert.That(ss, Is.Not.Null);
				Assert.That(ss.Length, Is.EqualTo(sval.Length));
				var s = ss.ToInsecureString();
				Assert.That(s, Is.EqualTo(sval));

				if (sval.Length > 1)
				{
					ss = ms.DangerousGetHandle().ToSecureString(1);
					Assert.That(ss, Is.Not.Null);
					Assert.That(ss.Length, Is.EqualTo(1));
					s = ss.ToInsecureString();
					Assert.That(s, Is.EqualTo(sval.Substring(0, 1)));
				}
			}
			else
			{
				Assert.That(ss, Is.Null);
			}
		}
	}
}