/*
 * Copyright (c) 2025 ETH Zürich, IT Services
 * 
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Net.Mime;
using System.Threading;
using CefSharp;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Browser.Contracts.Filters;
using SafeExamBrowser.Browser.Integrations;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Configuration.Contracts.Cryptography;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Settings;
using SafeExamBrowser.Settings.Browser;
using SafeExamBrowser.Settings.Browser.Filter;
using BrowserSettings = SafeExamBrowser.Settings.Browser.BrowserSettings;
using Request = SafeExamBrowser.Browser.Contracts.Filters.Request;
using ResourceHandler = SafeExamBrowser.Browser.Handlers.ResourceHandler;

namespace SafeExamBrowser.Browser.UnitTests.Handlers
{
	[TestClass]
	public class ResourceHandlerTests
	{
		private AppConfig appConfig;
		private Mock<IRequestFilter> filter;
		private Mock<IKeyGenerator> keyGenerator;
		private Mock<ILogger> logger;
		private BrowserSettings settings;
		private WindowSettings windowSettings;
		private Mock<IText> text;
		private TestableResourceHandler sut;

		[TestInitialize]
		public void Initialize()
		{
			var integrations = new Integration[]
			{
				new GenericIntegration(new Mock<ILogger>().Object),
				new EdxIntegration(new Mock<ILogger>().Object),
				new MoodleIntegration(new Mock<ILogger>().Object)
			};

			appConfig = new AppConfig();
			filter = new Mock<IRequestFilter>();
			keyGenerator = new Mock<IKeyGenerator>();
			logger = new Mock<ILogger>();
			settings = new BrowserSettings();
			windowSettings = new WindowSettings();
			text = new Mock<IText>();

			sut = new TestableResourceHandler(appConfig, filter.Object, integrations, keyGenerator.Object, logger.Object, SessionMode.Server, settings, windowSettings, text.Object);
		}

		[TestMethod]
		public void MustAppendCustomHeadersForSameDomain()
		{
			var browser = new Mock<IWebBrowser>();
			var headers = default(NameValueCollection);
			var request = new Mock<IRequest>();

			browser.SetupGet(b => b.Address).Returns("http://www.host.org");
			keyGenerator.Setup(g => g.CalculateBrowserExamKeyHash(It.IsAny<string>(), It.IsAny<byte[]>(), It.IsAny<string>())).Returns(new Random().Next().ToString());
			keyGenerator.Setup(g => g.CalculateConfigurationKeyHash(It.IsAny<string>(), It.IsAny<string>())).Returns(new Random().Next().ToString());
			request.SetupGet(r => r.Headers).Returns(new NameValueCollection());
			request.SetupGet(r => r.Url).Returns("http://www.host.org");
			request.SetupSet(r => r.Headers = It.IsAny<NameValueCollection>()).Callback<NameValueCollection>((h) => headers = h);
			settings.SendConfigurationKey = true;
			settings.SendBrowserExamKey = true;

			var result = sut.OnBeforeResourceLoad(browser.Object, Mock.Of<IBrowser>(), Mock.Of<IFrame>(), request.Object, Mock.Of<IRequestCallback>());

			request.VerifyGet(r => r.Headers, Times.AtLeastOnce);
			request.VerifySet(r => r.Headers = It.IsAny<NameValueCollection>(), Times.AtLeastOnce);

			Assert.AreEqual(CefReturnValue.Continue, result);
			Assert.IsNotNull(headers["X-SafeExamBrowser-ConfigKeyHash"]);
			Assert.IsNotNull(headers["X-SafeExamBrowser-RequestHash"]);
		}

		[TestMethod]
		public void MustAppendCustomHeadersForCrossDomainResourceRequestAndMainFrame()
		{
			var browser = new Mock<IWebBrowser>();
			var headers = new NameValueCollection();
			var request = new Mock<IRequest>();

			browser.SetupGet(b => b.Address).Returns("http://www.otherhost.org");
			keyGenerator.Setup(g => g.CalculateBrowserExamKeyHash(It.IsAny<string>(), It.IsAny<byte[]>(), It.IsAny<string>())).Returns(new Random().Next().ToString());
			keyGenerator.Setup(g => g.CalculateConfigurationKeyHash(It.IsAny<string>(), It.IsAny<string>())).Returns(new Random().Next().ToString());
			request.SetupGet(r => r.ResourceType).Returns(ResourceType.MainFrame);
			request.SetupGet(r => r.Headers).Returns(new NameValueCollection());
			request.SetupGet(r => r.Url).Returns("http://www.host.org");
			request.SetupSet(r => r.Headers = It.IsAny<NameValueCollection>()).Callback<NameValueCollection>((h) => headers = h);
			settings.SendConfigurationKey = true;
			settings.SendBrowserExamKey = true;

			var result = sut.OnBeforeResourceLoad(browser.Object, Mock.Of<IBrowser>(), Mock.Of<IFrame>(), request.Object, Mock.Of<IRequestCallback>());

			request.VerifyGet(r => r.Headers, Times.AtLeastOnce);
			request.VerifySet(r => r.Headers = It.IsAny<NameValueCollection>(), Times.AtLeastOnce);

			Assert.AreEqual(CefReturnValue.Continue, result);
			Assert.IsNotNull(headers["X-SafeExamBrowser-ConfigKeyHash"]);
			Assert.IsNotNull(headers["X-SafeExamBrowser-RequestHash"]);
		}

		[TestMethod]
		public void MustNotAppendCustomHeadersForCrossDomainResourceRequestAndSubResource()
		{
			var browser = new Mock<IWebBrowser>();
			var headers = new NameValueCollection();
			var request = new Mock<IRequest>();

			browser.SetupGet(b => b.Address).Returns("http://www.otherhost.org");
			request.SetupGet(r => r.ResourceType).Returns(ResourceType.SubResource);
			request.SetupGet(r => r.Headers).Returns(new NameValueCollection());
			request.SetupGet(r => r.Url).Returns("http://www.host.org");
			request.SetupSet(r => r.Headers = It.IsAny<NameValueCollection>()).Callback<NameValueCollection>((h) => headers = h);
			settings.SendConfigurationKey = true;
			settings.SendBrowserExamKey = true;

			var result = sut.OnBeforeResourceLoad(browser.Object, Mock.Of<IBrowser>(), Mock.Of<IFrame>(), request.Object, Mock.Of<IRequestCallback>());

			request.VerifyGet(r => r.Headers, Times.Never);
			request.VerifySet(r => r.Headers = It.IsAny<NameValueCollection>(), Times.Never);

			Assert.AreEqual(CefReturnValue.Continue, result);
			Assert.IsNull(headers["X-SafeExamBrowser-ConfigKeyHash"]);
			Assert.IsNull(headers["X-SafeExamBrowser-RequestHash"]);
		}

		[TestMethod]
		public void MustBlockMailToUrls()
		{
			var request = new Mock<IRequest>();
			var url = $"{Uri.UriSchemeMailto}:someone@somewhere.org";

			request.SetupGet(r => r.Url).Returns(url);

			var result = sut.OnBeforeResourceLoad(Mock.Of<IWebBrowser>(), Mock.Of<IBrowser>(), Mock.Of<IFrame>(), request.Object, Mock.Of<IRequestCallback>());

			Assert.AreEqual(CefReturnValue.Cancel, result);
		}

		[TestMethod]
		public void MustFilterContentRequests()
		{
			var request = new Mock<IRequest>();
			var url = "http://www.test.org";

			filter.Setup(f => f.Process(It.Is<Request>(r => r.Url.Equals(url)))).Returns(FilterResult.Block);
			request.SetupGet(r => r.ResourceType).Returns(ResourceType.SubFrame);
			request.SetupGet(r => r.Url).Returns(url);
			settings.Filter.ProcessContentRequests = true;
			settings.Filter.ProcessMainRequests = true;

			var resourceHandler = sut.GetResourceHandler(Mock.Of<IWebBrowser>(), Mock.Of<IBrowser>(), Mock.Of<IFrame>(), request.Object);

			filter.Verify(f => f.Process(It.Is<Request>(r => r.Url.Equals(url))), Times.Once);

			Assert.IsNotNull(resourceHandler);
		}

		[TestMethod]
		public void MustLetOperatingSystemHandleUnknownProtocols()
		{
			Assert.IsTrue(sut.OnProtocolExecution(default, default, default, default));
		}

		[TestMethod]
		public void MustRedirectToDisablePdfToolbar()
		{
			var frame = new Mock<IFrame>();
			var headers = new NameValueCollection { { "Content-Type", MediaTypeNames.Application.Pdf } };
			var request = new Mock<IRequest>();
			var response = new Mock<IResponse>();
			var url = "http://www.host.org/some-document";

			request.SetupGet(r => r.ResourceType).Returns(ResourceType.MainFrame);
			request.SetupGet(r => r.Url).Returns(url);
			response.SetupGet(r => r.Headers).Returns(headers);
			settings.AllowPdfReader = true;
			settings.AllowPdfReaderToolbar = false;

			var result = sut.OnResourceResponse(Mock.Of<IWebBrowser>(), Mock.Of<IBrowser>(), frame.Object, request.Object, response.Object);

			frame.Verify(b => b.LoadUrl(It.Is<string>(s => s.Equals($"{url}#toolbar=0"))), Times.Once);
			Assert.IsTrue(result);

			frame.Reset();
			request.SetupGet(r => r.Url).Returns($"{url}#toolbar=0");

			result = sut.OnResourceResponse(Mock.Of<IWebBrowser>(), Mock.Of<IBrowser>(), frame.Object, request.Object, response.Object);

			frame.Verify(b => b.LoadUrl(It.IsAny<string>()), Times.Never);
			Assert.IsFalse(result);
		}

		[TestMethod]
		public void MustReplaceSebScheme()
		{
			var request = new Mock<IRequest>();
			var url = default(string);

			appConfig.SebUriScheme = "abc";
			appConfig.SebUriSchemeSecure = "abcs";
			request.SetupGet(r => r.Headers).Returns(new NameValueCollection());
			request.SetupGet(r => r.Url).Returns($"{appConfig.SebUriScheme}://www.host.org");
			request.SetupSet(r => r.Url = It.IsAny<string>()).Callback<string>(u => url = u);

			var result = sut.OnBeforeResourceLoad(Mock.Of<IWebBrowser>(), Mock.Of<IBrowser>(), Mock.Of<IFrame>(), request.Object, Mock.Of<IRequestCallback>());

			Assert.AreEqual(CefReturnValue.Continue, result);
			Assert.AreEqual("http://www.host.org/", url);

			request.SetupGet(r => r.Url).Returns($"{appConfig.SebUriSchemeSecure}://www.host.org");
			request.SetupSet(r => r.Url = It.IsAny<string>()).Callback<string>(u => url = u);

			result = sut.OnBeforeResourceLoad(Mock.Of<IWebBrowser>(), Mock.Of<IBrowser>(), Mock.Of<IFrame>(), request.Object, Mock.Of<IRequestCallback>());

			Assert.AreEqual(CefReturnValue.Continue, result);
			Assert.AreEqual("https://www.host.org/", url);
		}

		[TestMethod]
		public void MustSearchGenericLmsSessionIdentifier()
		{
			var @event = new AutoResetEvent(false);
			var headers = new NameValueCollection();
			var newUrl = default(string);
			var request = new Mock<IRequest>();
			var response = new Mock<IResponse>();
			var userId = default(string);

			headers.Add("X-LMS-USER-ID", "some-session-id-123");
			request.SetupGet(r => r.Url).Returns("https://www.somelms.org");
			response.SetupGet(r => r.Headers).Returns(headers);
			sut.UserIdentifierDetected += (id) =>
			{
				userId = id;
				@event.Set();
			};

			sut.OnResourceRedirect(Mock.Of<IWebBrowser>(), Mock.Of<IBrowser>(), Mock.Of<IFrame>(), Mock.Of<IRequest>(), response.Object, ref newUrl);
			@event.WaitOne();
			Assert.AreEqual("some-session-id-123", userId);

			headers.Clear();
			headers.Add("X-LMS-USER-ID", "other-session-id-123");
			userId = default;

			sut.OnResourceResponse(Mock.Of<IWebBrowser>(), Mock.Of<IBrowser>(), Mock.Of<IFrame>(), request.Object, response.Object);
			@event.WaitOne();
			Assert.AreEqual("other-session-id-123", userId);
		}

		[TestMethod]
		public void MustSearchEdxSessionIdentifier()
		{
			var @event = new AutoResetEvent(false);
			var headers = new NameValueCollection();
			var newUrl = default(string);
			var request = new Mock<IRequest>();
			var response = new Mock<IResponse>();
			var userId = default(string);

			headers.Add("Set-Cookie", "edx-user-info=\"{\\\"username\\\": \\\"edx-123\\\"}\"; expires");
			request.SetupGet(r => r.Url).Returns("https://www.somelms.org");
			response.SetupGet(r => r.Headers).Returns(headers);
			sut.UserIdentifierDetected += (id) =>
			{
				userId = id;
				@event.Set();
			};

			sut.OnResourceRedirect(Mock.Of<IWebBrowser>(), Mock.Of<IBrowser>(), Mock.Of<IFrame>(), Mock.Of<IRequest>(), response.Object, ref newUrl);
			@event.WaitOne();
			Assert.AreEqual("edx-123", userId);

			headers.Clear();
			headers.Add("Set-Cookie", "edx-user-info=\"{\\\"username\\\": \\\"edx-345\\\"}\"; expires");
			userId = default;

			sut.OnResourceResponse(Mock.Of<IWebBrowser>(), Mock.Of<IBrowser>(), Mock.Of<IFrame>(), request.Object, response.Object);
			@event.WaitOne();
			Assert.AreEqual("edx-345", userId);
		}

		[TestMethod]
		public void MustSearchMoodleSessionIdentifier()
		{
			var @event = new AutoResetEvent(false);
			var headers = new NameValueCollection();
			var newUrl = default(string);
			var request = new Mock<IRequest>();
			var response = new Mock<IResponse>();
			var userId = default(string);

			headers.Add("Location", "https://www.some-moodle-instance.org/moodle/login/index.php?testsession=123");
			request.SetupGet(r => r.Url).Returns("https://www.some-moodle-instance.org");
			response.SetupGet(r => r.Headers).Returns(headers);
			sut.UserIdentifierDetected += (id) =>
			{
				userId = id;
				@event.Set();
			};

			sut.OnResourceRedirect(Mock.Of<IWebBrowser>(), Mock.Of<IBrowser>(), Mock.Of<IFrame>(), Mock.Of<IRequest>(), response.Object, ref newUrl);
			@event.WaitOne();
			Assert.AreEqual("123", userId);

			headers.Clear();
			headers.Add("Location", "https://www.some-moodle-instance.org/moodle/login/index.php?testsession=456");
			userId = default;

			sut.OnResourceResponse(Mock.Of<IWebBrowser>(), Mock.Of<IBrowser>(), Mock.Of<IFrame>(), request.Object, response.Object);
			@event.WaitOne();
			Assert.AreEqual("456", userId);
		}

		private class TestableResourceHandler : ResourceHandler
		{
			internal TestableResourceHandler(
				AppConfig appConfig,
				IRequestFilter filter,
				IEnumerable<Integration> integrations,
				IKeyGenerator keyGenerator,
				ILogger logger,
				SessionMode sessionMode,
				BrowserSettings settings,
				WindowSettings windowSettings,
				IText text) : base(appConfig, filter, integrations, keyGenerator, logger, sessionMode, settings, windowSettings, text)
			{
			}

			public new IResourceHandler GetResourceHandler(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IRequest request)
			{
				return base.GetResourceHandler(webBrowser, browser, frame, request);
			}

			public new CefReturnValue OnBeforeResourceLoad(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback)
			{
				return base.OnBeforeResourceLoad(webBrowser, browser, frame, request, callback);
			}

			public new bool OnProtocolExecution(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IRequest request)
			{
				return base.OnProtocolExecution(webBrowser, browser, frame, request);
			}

			public new void OnResourceRedirect(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response, ref string newUrl)
			{
				base.OnResourceRedirect(webBrowser, browser, frame, request, response, ref newUrl);
			}

			public new bool OnResourceResponse(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response)
			{
				return base.OnResourceResponse(webBrowser, browser, frame, request, response);
			}
		}
	}
}
