using ICSharpCode.Decompiler.TypeSystem;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Vanara.Extensions;
using Vanara.InteropServices;
using Windows.Storage.Streams;
using static Vanara.PInvoke.CldApi;

namespace Vanara.PInvoke.Tests
{
	public class CldApiTests
	{
		private static readonly string syncRootPath = Environment.GetEnvironmentVariable("OneDrive");
		//private const string tempSubDir = "CfSource";

		private static string SetupTempDir(string subDir) => Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), subDir)).FullName;

		private static void DeleteTempDir(string subDir)
		{
			var dirInfo = new DirectoryInfo(Path.Combine(Path.GetTempPath(), subDir));
			if (dirInfo.Exists) dirInfo.Delete(true);
		}

		[Test]
		public void CfConnectSyncRootTest()
		{
			const string dest = "CfDest";
			var destDirPath = SetupTempDir(dest);
			try
			{
				using var csp = new CloudSyncProvider(destDirPath, "TestSync");
				csp.Status = CF_SYNC_PROVIDER_STATUS.CF_PROVIDER_STATUS_IDLE;
				csp.Status.WriteValues();

				const string desc = "SyncStatus is good.";
				uint descLen = (uint)(desc.Length + 1) * 2;
				var ss = new CF_SYNC_STATUS { StructSize = (uint)Marshal.SizeOf<CF_SYNC_STATUS>() + descLen, Code = 1, DescriptionLength = descLen };
				var mem = new SafeHGlobalHandle(Marshal.SizeOf<CF_SYNC_STATUS>() + descLen);
				mem.Write(ss);
				StringHelper.Write(desc, ((IntPtr)mem).Offset(Marshal.SizeOf<CF_SYNC_STATUS>()), out _, true, CharSet.Unicode, descLen);
				Assert.That(CfReportSyncStatus(destDirPath, mem), ResultIs.Successful);
			}
			finally
			{
				DeleteTempDir(dest);
			}
		}

		[Test]
		public void CfCreatePlaceholdersTest()
		{
			const string dest = "CfDest";
			const string fname = "test.bmp";
			var destDirPath = SetupTempDir(dest);
			var fpath = Path.Combine(destDirPath, fname);
			try
			{
				var tokSrc = new CancellationTokenSource();
				var token = tokSrc.Token;
				var task = Task.Run(() =>
				{
					using var csp = new CloudSyncProvider(destDirPath, "TestSync");

					csp.CancelFetchData += ShowInfo;
					csp.CancelFetchPlaceholders += ShowInfo;
					csp.FetchData += ShowInfo;
					csp.FetchPlaceholders += ShowInfo;
					csp.NotifyDehydrate += ShowInfo;
					csp.NotifyDehydrateCompletion += ShowInfo;
					csp.NotifyDelete += ShowInfo;
					csp.NotifyDeleteCompletion += ShowInfo;
					csp.NotifyFileCloseCompletion += ShowInfo;
					csp.NotifyFileOpenCompletion += ShowInfo;
					csp.NotifyRename += ShowInfo;
					csp.NotifyRenameCompletion += ShowInfo;
					csp.ValidateData += ShowInfo;

					var origFileInfo = new FileInfo(TestCaseSources.BmpFile);
					csp.CreatePlaceholderFromFile(fname, origFileInfo, true);
					Assert.That(File.Exists(fpath), Is.True);
					Assert.That(new FileInfo(fpath).Length, Is.EqualTo(origFileInfo.Length));

					Debug.WriteLine("CSP is running...................................\n");

					while (!token.IsCancellationRequested) { Thread.Sleep(100); }

					static void ShowInfo<T>(object s, CloudSyncCallbackArgs<T> e) where T : struct
					{
						Debug.WriteLine($"\n{typeof(T).Name}: {e.NormalizedPath ?? "(null)"}, {e.FileSize}\n" +
							Newtonsoft.Json.JsonConvert.SerializeObject(e.ParamData, Newtonsoft.Json.Formatting.Indented, new Newtonsoft.Json.Converters.StringEnumConverter()));

						if (typeof(T) == typeof(CF_CALLBACK_PARAMETERS.RENAME))
						{
							e.OperationType = CF_OPERATION_TYPE.CF_OPERATION_TYPE_ACK_RENAME;
							e.OpParam = CF_OPERATION_PARAMETERS.Create(new CF_OPERATION_PARAMETERS.ACKRENAME());
						}
						else if (typeof(T) == typeof(CF_CALLBACK_PARAMETERS.DEHYDRATE))
						{
							e.OperationType = CF_OPERATION_TYPE.CF_OPERATION_TYPE_ACK_DEHYDRATE;
							e.OpParam = CF_OPERATION_PARAMETERS.Create(new CF_OPERATION_PARAMETERS.ACKDEHYDRATE());
						}
						else if (typeof(T) == typeof(CF_CALLBACK_PARAMETERS.DELETE))
						{
							e.OperationType = CF_OPERATION_TYPE.CF_OPERATION_TYPE_ACK_DELETE;
							e.OpParam = CF_OPERATION_PARAMETERS.Create(new CF_OPERATION_PARAMETERS.ACKDELETE());
						}
						else if (typeof(T) == typeof(CF_CALLBACK_PARAMETERS.FETCHDATA))
						{
							var opInfo = e.MakeOpInfo(CF_OPERATION_TYPE.CF_OPERATION_TYPE_TRANSFER_DATA);
							using var buf = new PinnedObject(File.ReadAllBytes(TestCaseSources.BmpFile));
							var opParam = CF_OPERATION_PARAMETERS.Create(new CF_OPERATION_PARAMETERS.TRANSFERDATA { Buffer = buf, Length = new FileInfo(TestCaseSources.BmpFile).Length });
							var hr = CfExecute(opInfo, ref opParam);
							if (hr.Failed) Debug.WriteLine("CfExecute for transfer failed: " + hr.FormatMessage());
							hr = CfReportProviderProgress(e.ConnectionKey, e.TransferKey, 100, 100);
							if (hr.Failed) Debug.WriteLine("CfReportProviderProgress for transfer failed: " + hr.FormatMessage());
						}
					}
				}, tokSrc.Token);

				Thread.Sleep(5000); // Let CSP get loaded
				using var hFile = GetHFILE(fpath);
				Assert.That(CfHydratePlaceholder(hFile, 0, -1, 0), ResultIs.Successful);
				Assert.That(CfGetCorrelationVector(hFile, out var cv), ResultIs.Successful);
				using var buf = new SafeHGlobalHandle(128);
				Assert.That(CfGetPlaceholderRangeInfo(hFile, CF_PLACEHOLDER_RANGE_INFO_CLASS.CF_PLACEHOLDER_RANGE_INFO_ONDISK, 0, buf.Size, buf, buf.Size, out var rngLen), ResultIs.Successful);
				Assert.That(CfGetTransferKey(hFile, out var txKey), ResultIs.Successful);
				Assert.That(() => CfReleaseTransferKey(hFile, txKey), Throws.Nothing);
				Assert.That(CfDehydratePlaceholder(hFile, 0, -1, 0), ResultIs.Successful);
				Thread.Sleep(2000); // Wait for user interaction

				tokSrc.Cancel();
				task.Wait();

				//CfGetPlaceholderRangeInfo(hFile, CF_PLACEHOLDER_RANGE_INFO_CLASS.CF_PLACEHOLDER_RANGE_INFO_ONDISK, 0, )
				//Assert.That(CfHydratePlaceholder(hFile, 0, -1, CF_HYDRATE_FLAGS.CF_HYDRATE_FLAG_NONE), ResultIs.Successful);
			}
			finally
			{
				DeleteTempDir(dest);
			}
		}

		private string CopyFile(string src, string destDir)
		{
			var destFile = Path.Combine(destDir, Path.GetFileName(src));
			File.Copy(src, destFile);
			Assert.That(File.Exists(destFile), Is.True);
			return destFile;
		}

		private Kernel32.SafeHFILE GetHFILE(string path) => Kernel32.CreateFile(path, Kernel32.FileAccess.FILE_READ_ATTRIBUTES, FileShare.Read, null, FileMode.Open, 0);

		[Test]
		public void CfGetPlaceholderInfoTest()
		{
			const string dest = "CfDest";
			var destDirPath = SetupTempDir(dest);
			try
			{
				using var csp = new CloudSyncProvider(destDirPath, "TestSync");

				// Copy in file and convert to placeholder
				var destFile = CopyFile(TestCaseSources.WordDoc, destDirPath);
				Assert.That(() => csp.ConvertToPlaceholder(destFile), Throws.Nothing);

				using var hFile = GetHFILE(destFile);
				Assert.That(() => CfGetPlaceholderInfo<CF_PLACEHOLDER_BASIC_INFO>(hFile).WriteValues(), Throws.Nothing);
				Assert.That(() => CfGetPlaceholderInfo<CF_PLACEHOLDER_STANDARD_INFO>(hFile).WriteValues(), Throws.Nothing);
			}
			finally
			{
				DeleteTempDir(dest);
			}
		}

		[Test]
		public void CfGetPlaceholderStateFromFindDataTest()
		{
			const string dest = "CfDest";
			var destDirPath = SetupTempDir(dest);
			try
			{
				using var csp = new CloudSyncProvider(destDirPath, "TestSync");
				csp.CreatePlaceholderFromFile("test.bmp", new FileInfo(TestCaseSources.BmpFile), true);
				Assert.That(File.Exists(Path.Combine(destDirPath, "test.bmp")), Is.True);

				Kernel32.FindFirstFile(Path.Combine(destDirPath, "test.bmp"), out var findData).Dispose();
				CfGetPlaceholderStateFromFindData(findData).WriteValues();
			}
			finally
			{
				DeleteTempDir(dest);
			}
		}

		[Test]
		public void CfSetInSyncStateTest()
		{
			const string dest = "CfDest";
			var destDirPath = SetupTempDir(dest);
			try
			{
				using var csp = new CloudSyncProvider(destDirPath, "TestSync");
				csp.CreatePlaceholderFromFile("test.bmp", new FileInfo(TestCaseSources.BmpFile), true);
				var destFile = Path.Combine(destDirPath, "test.bmp");
				Assert.That(File.Exists(destFile), Is.True);

				using var hFile = GetHFILE(destFile);
				var usn = 0;
				Assert.That(CfSetInSyncState(hFile, CF_IN_SYNC_STATE.CF_IN_SYNC_STATE_IN_SYNC, CF_SET_IN_SYNC_FLAGS.CF_SET_IN_SYNC_FLAG_NONE, ref usn), ResultIs.Successful);
				Assert.That(CfSetPinState(hFile, CF_PIN_STATE.CF_PIN_STATE_PINNED, CF_SET_PIN_FLAGS.CF_SET_PIN_FLAG_NONE), ResultIs.Successful);
			}
			finally
			{
				DeleteTempDir(dest);
			}
		}

		[Test]
		public void CfGetPlaceholderStateFromAttributeTagTest()
		{
			const string dest = "CfDest";
			var destDirPath = SetupTempDir(dest);
			try
			{
				using var csp = new CloudSyncProvider(destDirPath, "TestSync");
				var destFile = CopyFile(TestCaseSources.WordDoc, destDirPath);
				Assert.That(() => csp.ConvertToPlaceholder(destFile), Throws.Nothing);

				using var hFile = GetHFILE(destFile);
				Kernel32.FILE_ATTRIBUTE_TAG_INFO info = default;
				Assert.That(() => info = Kernel32.GetFileInformationByHandleEx<Kernel32.FILE_ATTRIBUTE_TAG_INFO>(hFile, Kernel32.FILE_INFO_BY_HANDLE_CLASS.FileAttributeTagInfo), Throws.Nothing);
				info.WriteValues();
				CfGetPlaceholderStateFromAttributeTag(info.FileAttributes, info.ReparseTag).WriteValues();

				using var mem = SafeHGlobalHandle.CreateFromStructure(info);
				CfGetPlaceholderStateFromFileInfo(mem, Kernel32.FILE_INFO_BY_HANDLE_CLASS.FileAttributeTagInfo).WriteValues();
			}
			finally
			{
				DeleteTempDir(dest);
			}
		}

		[Test]
		public void CfGetPlatformInfoTest()
		{
			Assert.That(CfGetPlatformInfo(out var info), ResultIs.Successful);
			info.WriteValues();
		}

		[Test]
		public void CfGetSyncRootInfoByHandleTest()
		{
			using var hFile = GetHFILE(new DirectoryInfo(syncRootPath).EnumerateFiles("*.*").First().FullName);
			Assert.That(hFile, ResultIs.ValidHandle);
			using (var mem = SafeHGlobalHandle.CreateFromStructure<CF_SYNC_ROOT_BASIC_INFO>())
			{
				Assert.That(CfGetSyncRootInfoByHandle(hFile, CF_SYNC_ROOT_INFO_CLASS.CF_SYNC_ROOT_INFO_BASIC, mem, mem.Size, out var len), ResultIs.Successful);
				mem.ToStructure<CF_SYNC_ROOT_BASIC_INFO>().WriteValues();
			}
			Assert.That(() => CfGetSyncRootInfoByHandle<CF_SYNC_ROOT_PROVIDER_INFO>(hFile).WriteValues(), Throws.Nothing);
			Assert.That(() => CfGetSyncRootInfoByHandle<CF_SYNC_ROOT_STANDARD_INFO>(hFile).WriteValues(), Throws.Nothing);
		}

		[Test]
		public void CfGetSyncRootInfoByPathTest()
		{
			using (var mem = SafeHGlobalHandle.CreateFromStructure<CF_SYNC_ROOT_BASIC_INFO>())
			{
				Assert.That(CfGetSyncRootInfoByPath(syncRootPath, CF_SYNC_ROOT_INFO_CLASS.CF_SYNC_ROOT_INFO_BASIC, mem, mem.Size, out var len), ResultIs.Successful);
				mem.ToStructure<CF_SYNC_ROOT_BASIC_INFO>().WriteValues();
			}
			Assert.That(() => CfGetSyncRootInfoByPath<CF_SYNC_ROOT_PROVIDER_INFO>(syncRootPath).WriteValues(), Throws.Nothing);
			Assert.That(() => CfGetSyncRootInfoByPath<CF_SYNC_ROOT_STANDARD_INFO>(syncRootPath).WriteValues(), Throws.Nothing);
		}

		[Test]
		public void CfOpenFileWithOplockTest()
		{
			Assert.That(CfOpenFileWithOplock(TestCaseSources.SmallFile, CF_OPEN_FILE_FLAGS.CF_OPEN_FILE_FLAG_EXCLUSIVE, out var handle), ResultIs.Successful);
			Assert.That(handle, ResultIs.ValidHandle);
			Assert.That(CfReferenceProtectedHandle(handle), ResultIs.Successful);
			Assert.That(CfGetWin32HandleFromProtectedHandle(handle), ResultIs.ValidHandle);
			Assert.That(() => CfReleaseProtectedHandle(handle), Throws.Nothing);
			Assert.That(() => handle.Dispose(), Throws.Nothing);
		}
	}
}