using NUnit.Framework;
using NUnit.Framework.Internal;
using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Vanara.Extensions;
using Vanara.InteropServices;
using Vanara.PInvoke;
using Vanara.PInvoke.Tests;
using static Vanara.PInvoke.Kernel32;

namespace Vanara.Diagnostics.Tests
{
	[TestFixture]
	public class JobTests
	{
		[Test]
		public void ActiveProcessLimitTest()
		{
			using var job = Job.Create();

			job.RuntimeLimits.ActiveProcessLimit = 2;
			Assert.That(job.RuntimeLimits.ActiveProcessLimit.Value, Is.EqualTo(2U));
			job.RuntimeLimits.ActiveProcessLimit = 0;
			Assert.That(job.RuntimeLimits.ActiveProcessLimit.Value, Is.EqualTo(0U));
			Assert.That(() => job.StartProcess("notepad.exe"), Throws.Exception);
			job.RuntimeLimits.ActiveProcessLimit = null;
			Assert.That(job.RuntimeLimits.ActiveProcessLimit.HasValue, Is.False);
		}

		[Test]
		public void EventsTest()
		{
			using var job = Job.Create(Environment.UserName);
			try
			{
				job.NewProcess += msgHndlr;
				job.ProcessExited += msgHndlr;
				job.JobMemoryLimitExceeded += msgHndlr;
				job.JobNotificationLimitExceeded += notHndlr;

				var startInfo = new ProcessStartInfo("notepad.exe") { UseShellExecute = false };
				var p1 = new Process { StartInfo = startInfo };
				p1.StartEx(CREATE_PROCESS.CREATE_SUSPENDED);
				job.AssignProcess(p1);
				p1.ResumePrimaryThread();

				Thread.Sleep(200);

				job.Notifications.JobMemoryLimit = job.Statistics.PeakJobMemoryUsed;
				job.RuntimeLimits.JobMemoryLimit = job.Notifications.JobMemoryLimit * 3;
				//TestContext.WriteLine($"JobMemory: NotLim: {job.JobMemory.NotificationLimit}, Lim: {job.JobMemory.Limit}");

				//job.PerJobUserTime.NotificationLimit = job.Statistics.TotalUserTime;
				//job.PerJobUserTime.Limit = TimeSpan.FromTicks(job.PerJobUserTime.NotificationLimit.Value.Ticks * 3);
				//TestContext.WriteLine($"PerJobUserTime: NotLim: {job.PerJobUserTime.NotificationLimit}, Lim: {job.PerJobUserTime.Limit}");

				var p2 = new Process { StartInfo = startInfo };
				p2.StartEx(CREATE_PROCESS.CREATE_SUSPENDED);
				job.AssignProcess(p2);
				p2.ResumePrimaryThread();

				Thread.Sleep(2000);
				Thread.Yield();

				p1.Kill();
				Thread.Sleep(200);
				Thread.Yield();
			}
			finally
			{
				job.TerminateAllProcesses(0);
			}

			static void msgHndlr(object s, JobEventArgs e) => TestContext.WriteLine($"{DateTime.Now:u}: {e.JobMessage}, {e.ProcessId}");
			static void notHndlr(object s, JobNotificationEventArgs e) => TestContext.WriteLine($"{DateTime.Now:u}: {e.JobMessage}, {e.Limit}, Limit: {e.NotificationLimit}, Val: {e.ReportedValue}");
		}

		[Test]
		public void LimitsTest()
		{
			using var job = Job.Create();
			try
			{
				Assert.That(job.Processes.Count, Is.EqualTo(0));
				var p1 = TestHelper.RunThrottleApp();
				job.AssignProcess(p1);
				Thread.Sleep(200);

				job.RuntimeLimits.ActiveProcessLimit = 1;
				Assert.That(() => job.StartProcess("notepad.exe"), Throws.Exception);
				job.RuntimeLimits.ActiveProcessLimit = null;

				Test(job.RuntimeLimits, 1.0, (s, v) => s.CpuRateLimit = v, s => s.CpuRateLimit, true);
				Test(job.RuntimeLimits, (25.0, 75.0), (s, v) => s.CpuRatePortion = v, s => s.CpuRatePortion, true);
				Test(job.RuntimeLimits, 3, (s, v) => s.CpuRateRelativeWeight = v, s => s.CpuRateRelativeWeight, true);
				Test(job.RuntimeLimits, 4096UL, (s, v) => s.JobMemoryLimit = v, s => s.JobMemoryLimit, true);
				Test(job.RuntimeLimits, 4096UL, (s, v) => s.MaxBandwidth = v, s => s.MaxBandwidth);
				TestGT(job.RuntimeLimits, TimeSpan.FromTicks(30000), (s, v) => s.PerJobUserTimeLimit = v, s => s.PerJobUserTimeLimit);
				TestGT(job.RuntimeLimits, TimeSpan.FromTicks(30000), (s, v) => s.PerProcessUserTimeLimit = v, s => s.PerProcessUserTimeLimit);
				Test(job.RuntimeLimits, 4096UL, (s, v) => s.ProcessMemoryLimit = v, s => s.ProcessMemoryLimit, true);
				var pmc = PROCESS_MEMORY_COUNTERS.Default;
				GetProcessMemoryInfo(p1, out pmc, pmc.cb);
				Test(job.RuntimeLimits, (pmc.WorkingSetSize, pmc.PeakWorkingSetSize), (s, v) => s.WorkingSetSize = v, s => s.WorkingSetSize, true);
			}
			finally
			{
				job.TerminateAllProcesses(0);
				Thread.Sleep(200);
			}

			void TestGT<T, TS>(TS js, T? value, Action<TS, T?> set, Func<TS, T?> get) where T : struct
			{
				Assert.That(() => set(js, value), Throws.Nothing);
				Assert.That(get(js).Value, Is.GreaterThanOrEqualTo(value));

				Assert.That(() => set(js, null), Throws.Nothing);
				Assert.That(get(js), Is.Null);
			}
		}

		[Test]
		public void NotificationTest()
		{
			using var job = Job.Create();
			try
			{
				job.NewProcess += (s, e) => TestContext.WriteLine($"{DateTime.Now:u}: {e.JobMessage}, {e.ProcessId}");
				job.JobNotificationLimitExceeded += notHndlr;

				job.Notifications.IoRateControlTolerance = JOBOBJECT_RATE_CONTROL_TOLERANCE.ToleranceLow;
				job.Notifications.IoRateControlToleranceInterval = JOBOBJECT_RATE_CONTROL_TOLERANCE_INTERVAL.ToleranceIntervalShort;
				//job.Notifications.IoReadBytesLimit = 1;
				//job.Notifications.IoWriteBytesLimit = 1;
				job.Notifications.JobMemoryLimit = 8092;
				job.Notifications.JobLowMemoryLimit = 4096;
				job.Notifications.NetRateControlTolerance = JOBOBJECT_RATE_CONTROL_TOLERANCE.ToleranceLow;
				job.Notifications.NetRateControlToleranceInterval = JOBOBJECT_RATE_CONTROL_TOLERANCE_INTERVAL.ToleranceIntervalShort;
				//job.Notifications.PerJobUserTimeLimit = TimeSpan.FromMilliseconds(1);
				//job.Notifications.RateControlTolerance = JOBOBJECT_RATE_CONTROL_TOLERANCE.ToleranceLow;
				//job.Notifications.RateControlToleranceInterval = JOBOBJECT_RATE_CONTROL_TOLERANCE_INTERVAL.ToleranceIntervalShort;

				job.StartProcess("notepad.exe");

				for (int i = 0; i < 10; i++)
				{
					Thread.Sleep(1000);
					Thread.Yield();
				}
			}
			finally
			{
				job.TerminateAllProcesses(0);
			}

			static void notHndlr(object s, JobNotificationEventArgs e) => TestContext.WriteLine($"{DateTime.Now:u}: {e.JobMessage}, {e.Limit}, Limit: {e.NotificationLimit}, Val: {e.ReportedValue}");
		}

		[Test]
		public void OpenJobTest()
		{
			using var job = Job.Create(Environment.UserName);
			Assert.That(job.Handle, ResultIs.ValidHandle);
			//using var job1 = Job.Open(Environment.UserName, JobAccessRight.JOB_OBJECT_QUERY);
			//Assert.That(job1.Handle, ResultIs.ValidHandle);

			Assert.That(() => job.Settings.GroupAffinity, Throws.Nothing);
		}

		[Test]
		public void ProcessesTest()
		{
			Process p2 = null;
			using (var job = Job.Create())
			{
				job.Settings.KillOnJobClose = true;

				var curProc = Process.GetCurrentProcess();
				Assert.That(job.ContainsProcess(curProc), Is.False);

				var p1 = Process.Start("notepad.exe");
				job.AssignProcess(p1);
				job.AssignProcess(p2 = Process.Start("notepad.exe"));

				Assert.That(job.Processes.Count, Is.EqualTo(2));
				Assert.That(job.Processes.Count(), Is.EqualTo(2));
				Assert.That(job.Processes.First().Id, Is.EqualTo(p1.Id));

				p1.Kill();
				Assert.That(p1.WaitForExit(500), Is.True);
				Assert.That(job.Processes.Count, Is.EqualTo(1));
				Assert.That(job.Processes.Count(), Is.EqualTo(1));
			}
			Assert.That(p2.WaitForExit(500), Is.True);
		}

		[Test]
		public void SettingsMixTest()
		{
			using var job = Job.Create();
			Assert.That(() => job.Settings.Affinity = (UIntPtr)0x2, Throws.Nothing);
			job.Settings.KillOnJobClose = true;
			Assert.That(() => job.Settings.Affinity = (UIntPtr)0x2, Throws.Nothing);
		}

		[Test]
		public void SettingsTest()
		{
			using var job = Job.Create();

			Test(job.Settings, new UIntPtr(0xf), (s, v) => s.Affinity = v, s => s.Affinity, true);
			TestBool(job.Settings, (s, v) => s.ChildProcessBreakawayAllowed = v, s => s.ChildProcessBreakawayAllowed);
			TestBool(job.Settings, (s, v) => s.ChildProcessSilentBreakawayAllowed = v, s => s.ChildProcessSilentBreakawayAllowed);
			TestBool(job.Settings, (s, v) => s.DieOnUnhandledException = v, s => s.DieOnUnhandledException);
			Test(job.Settings, 0xf, (s, v) => s.DscpTag = v, s => s.DscpTag);
			TestBool(job.Settings, (s, v) => s.KillOnJobClose = v, s => s.KillOnJobClose);
			Test(job.Settings, ProcessPriorityClass.BelowNormal, (s, v) => s.PriorityClass = v, s => s.PriorityClass, true);
			Test(job.Settings, 3, (s, v) => s.SchedulingClass = v, s => s.SchedulingClass);
			TestBool(job.Settings, (s, v) => s.TerminateProcessesAtEndOfJobTimeLimit = v, s => s.TerminateProcessesAtEndOfJobTimeLimit);
			Assert.That(() => job.Settings.UIRestrictionsClass = JOBOBJECT_UILIMIT_FLAGS.JOB_OBJECT_UILIMIT_ALL, Throws.Nothing);
			Assert.That(job.Settings.UIRestrictionsClass, Is.EqualTo(JOBOBJECT_UILIMIT_FLAGS.JOB_OBJECT_UILIMIT_ALL));

			Assert.That(() => job.Settings.GroupAffinity.First(), Throws.Nothing);
			Assert.That(job.Settings.GroupAffinity.First().Mask.ToUInt32(), Is.Not.Zero);
			Assert.That(() => job.Settings.GroupAffinity = job.Settings.GroupAffinity, Throws.Nothing);
		}

		private void Test<T, TS>(TS js, T? value, Action<TS, T?> set, Func<TS, T?> get, bool ignDef = false) where T : struct
		{
			Assert.That(() => set(js, value), Throws.Nothing);
			Assert.That(get(js).Value, Is.EqualTo(value));

			if (!ignDef)
			{
				Assert.That(() => set(js, default(T)), Throws.Nothing);
				Assert.That(get(js).Value, Is.EqualTo(default(T)));
			}

			Assert.That(() => set(js, null), Throws.Nothing);
			Assert.That(get(js), Is.Null);
		}

		private void TestBool<TS>(TS js, Action<TS, bool> set, Func<TS, bool> get)
		{
			var orig = get(js);

			Assert.That(() => set(js, !orig), Throws.Nothing);
			Assert.That(get(js), Is.EqualTo(!orig));

			Assert.That(() => set(js, orig), Throws.Nothing);
			Assert.That(get(js), Is.EqualTo(orig));
		}

		[Test]
		public void StatsTest()
		{
			using var job = Job.Create();
			try
			{
				job.AssignProcess(Process.Start("notepad.exe"));
				Thread.Sleep(200);
			}
			finally
			{
				job.TerminateAllProcesses(0);
				Thread.Sleep(200);
			}
			var stats = job.Statistics;
			stats.WriteValues();
			Assert.That(stats.PeakJobMemoryUsed, Is.GreaterThan(0UL));
			Assert.That(stats.PeakProcessMemoryUsed, Is.GreaterThan(0UL));
			Assert.That(stats.ThisPeriodTotalKernelTime.Ticks, Is.GreaterThan(0UL));
			Assert.That(stats.ThisPeriodTotalUserTime.Ticks, Is.GreaterThanOrEqualTo(0UL));
			Assert.That(stats.TotalKernelTime.Ticks, Is.GreaterThan(0UL));
			Assert.That(stats.TotalPageFaultCount, Is.GreaterThanOrEqualTo(0UL));
			Assert.That(stats.TotalProcesses, Is.GreaterThan(0UL));
			Assert.That(stats.TotalTerminatedProcesses, Is.GreaterThanOrEqualTo(0UL));
			Assert.That(stats.TotalUserTime.Ticks, Is.GreaterThanOrEqualTo(0UL));
		}

		[Test]
		public void TempTest()
		{
			using var job = Job.Create();
			var str = new JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION_2 { LimitFlags = JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_MEMORY_LOW, JobLowMemoryLimit = 4096 };
			using var mem = SafeHGlobalHandle.CreateFromStructure(str);
			if (!SetInformationJobObject(job, JOBOBJECTINFOCLASS.JobObjectNotificationLimitInformation2, mem, mem.Size))
				TestContext.WriteLine($"{Win32Error.GetLastError()}");

			mem.Zero();
			if (QueryInformationJobObject(job, JOBOBJECTINFOCLASS.JobObjectNotificationLimitInformation2, mem, mem.Size, out _))
				mem.ToStructure<JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION_2>().WriteValues();

			str.LimitFlags = 0;
			str.JobLowMemoryLimit = 0;
			mem.Write(str);
			if (!SetInformationJobObject(job, JOBOBJECTINFOCLASS.JobObjectNotificationLimitInformation2, mem, mem.Size))
				TestContext.WriteLine($"{Win32Error.GetLastError()}");

			mem.Zero();
			if (QueryInformationJobObject(job, JOBOBJECTINFOCLASS.JobObjectNotificationLimitInformation2, mem, mem.Size, out _))
				mem.ToStructure<JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION_2>().WriteValues();
		}
	}
}