Dan Stace

using KSG.RoverTwo.Enums;
using KSG.RoverTwo.Exceptions;
using KSG.RoverTwo.Models;
using KSG.RoverTwo.Tests.Extensions;
using Build = KSG.RoverTwo.Tests.Helpers.ProblemBuilder;
using Task = KSG.RoverTwo.Models.Task;

namespace KSG.RoverTwo.Tests;

public class ProblemTests : TestBase
{
	[Fact]
	public void Problem_WithAllDefaults_PassesValidation()
	{
		var problem = BasicProblem().Fill();
		problem.Validate();
		Assert.True(true);
	}

	[Fact]
	public void Jobs_WhenEmpty_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		problem.Jobs.Clear();
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains($"jobs is Missing", exception.Message);
	}

	[Fact]
	public void Job_WithDuplicateId_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		var firstJob = problem.Jobs.First();
		problem.Jobs.Add(Build.Job(firstJob.Id));
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains($"jobs#1.id={firstJob.Id} is NotUnique", exception.Message);
	}

	[Fact]
	public void Job_WithArrivalWindowCloseBeforeOpen_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		var place = problem.Jobs.First();
		var tZero = new DateTimeOffset();
		place.ArrivalWindow.Open = tZero.AddHours(2);
		place.ArrivalWindow.Close = tZero.AddHours(1);
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains($"jobs#0.arrivalWindow.close is before open", exception.Message);
	}

	[Fact]
	public void Hub_WhenDistanceMatrixIsRequiredByFactor_RequiresLocation()
	{
		var problem = BasicProblem().Fill();
		problem.Hubs.First().Location = null;
		Assert.True(problem.IsDistanceMatrixRequired);
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains("hubs#0.location is Missing", exception.Message);
	}

	[Fact]
	public void Job_WithoutTasks_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		var noTaskJob = Build.Job();
		problem.Jobs.Insert(0, noTaskJob);
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains("jobs#0.tasks is MissingOrEmpty", exception.Message);
	}

	[Fact]
	public void Task_WithDuplicateId_FailsValidation()
	{
		var tool = Build.Tool();
		var problem = BasicProblem().WithAddedTool(tool);
		var firstTask = Build.Task();
		var secondTask = new Task()
		{
			Id = firstTask.Id,
			ToolId = tool.Id,
			Rewards = [],
		};
		problem.WithJobs([Build.Job().WithTasks([firstTask, secondTask])]);
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Fill().Validate();
		});
		Assert.Contains($"jobs#0.tasks#1.id={firstTask.Id} is NotUnique", exception.Message);
	}

	[Fact]
	public void Hub_WithDuplicateId_FailsValidation()
	{
		var problem = BasicProblem();
		var hub = Build.Hub();
		problem.WithHubs([hub, hub]).Fill();
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains($"hubs#1.id={hub.Id} is NotUnique", exception.Message);
	}

	[Fact]
	public void Task_WithNonexistentToolId_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		var firstJob = problem.Jobs.First();
		var firstTask = firstJob.Tasks.First();
		firstTask.ToolId = Guid.NewGuid().ToString();
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains($"jobs#0.tasks#0.toolId={firstTask.ToolId} is Unrecognized", exception.Message);
	}

	[Fact]
	public void Task_WithNoRewards_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		var firstJob = problem.Jobs.First();
		var firstTask = firstJob.Tasks.First();
		firstTask.Rewards.Clear();
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains("jobs#0.tasks#0.rewards is MissingOrEmpty", exception.Message);
	}

	[Fact]
	public void Task_WithNegativeReward_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		var firstJob = problem.Jobs.First();
		var firstTask = firstJob.Tasks.First();
		firstTask.Rewards.First().Amount = -1;
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains("jobs#0.tasks#0.rewards#0.amount=-1 is LessThanZero", exception.Message);
	}

	[Fact]
	public void Task_WithNonexistentRewardMetric_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		var firstJob = problem.Jobs.First();
		var firstTask = firstJob.Tasks.First();
		var firstReward = firstTask.Rewards.First();
		firstReward.MetricId = Guid.NewGuid().ToString();
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains($"jobs#0.tasks#0.rewards#0.metricId={firstReward.MetricId} is Unrecognized", exception.Message);
	}

	[Fact]
	public void Tools_MissingOrEmpty_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		problem.Tools.Clear();
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains($"tools is Missing", exception.Message);
	}

	[Fact]
	public void Tools_EmptyId_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		var json = problem.AsJsonNode();
		json["tools"][0]["id"] = " ";
		var exception = Assert.Throws<ValidationError>(() =>
		{
			Problem.FromJson(json.ToString()).Validate();
		});
		Assert.Contains("tools#0.id is MissingOrEmpty", exception.Message);
	}

	[Fact]
	public void Tools_DuplicateId_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		problem.Tools.Add(problem.Tools.First());
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains($"tools#1.id={problem.Tools[0].Id} is NotUnique", exception.Message);
	}

	[Fact]
	public void Tools_NegativeDefaultWorkTime_FailsValidation()
	{
		var tool = Build.Tool(defaultWorkTime: -1);
		var problem = Build.Problem().WithAddedTool(tool).Fill();
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains($"tools#0.defaultWorkTime=-1 is LessThanOrEqualToZero", exception.Message);
	}

	[Fact]
	public void Tools_NegativeDefaultCompletionChance_FailsValidation()
	{
		var tool = Build.Tool(defaultCompletionChance: -1);
		var problem = Build.Problem().WithAddedTool(tool).Fill();
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains($"tools#0.defaultCompletionChance=-1 is LessThanOrEqualToZero", exception.Message);
	}

	[Fact]
	public void Problem_WithNoWorkers_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		problem.Workers.Clear();
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains($"workers is Missing", exception.Message);
	}

	[Fact]
	public void Worker_WithDuplicateId_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		var firstWorker = problem.Workers.First();
		problem.Workers.Add(Build.Worker(firstWorker.Id));
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains($"workers#1.id={firstWorker.Id} is NotUnique", exception.Message);
	}

	[Fact]
	public void Worker_WithNonexistentStartHubId_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		var firstHub = problem.Hubs.First();
		var worker = Build.Worker(endHub: firstHub);
		problem.WithWorker(worker);
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains(
			$"workers#{problem.Workers.Count - 1}.startHubId={worker.StartHubId} is Unrecognized",
			exception.Message
		);
	}

	[Fact]
	public void Worker_WithStartTimeAfterEndTime_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		var tZero = new DateTimeOffset();
		var worker = Build.Worker();
		worker.EarliestStartTime = tZero.AddHours(2);
		worker.LatestEndTime = tZero.AddHours(1);
		problem.WithWorker(worker);
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains(
			$"workers#{problem.Workers.Count - 1}.earliestStartTime is after latestEndTime",
			exception.Message
		);
	}

	[Fact]
	public void Worker_WithNonexistentEndHubId_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		var firstHub = problem.Hubs.First();
		var hub = Build.Hub();
		var worker = Build.Worker(startHub: firstHub, endHub: hub);
		problem.WithWorker(worker);
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains(
			$"workers#{problem.Workers.Count - 1}.endHubId={worker.EndHubId} is Unrecognized",
			exception.Message
		);
	}

	[Fact]
	public void Worker_WithNegativeTravelSpeedFactor_FailsValidation()
	{
		var worker = Build.Worker(travelSpeedFactor: -1);
		var problem = BasicProblem().WithWorker(worker).Fill();
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains(
			$"workers#{problem.Workers.Count - 1}.travelSpeedFactor is LessThanOrEqualToZero",
			exception.Message
		);
	}

	[Fact]
	public void Worker_WithNoCapabilities_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		var worker = problem.Workers.First();
		worker.Capabilities.Clear();
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains($"workers#0.capabilities is MissingOrEmpty", exception.Message);
	}

	[Fact]
	public void Worker_CapabilityWithNonexistentTool_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		var worker = problem.Workers.First();
		var tool = Build.Tool();
		worker.WithAddedCapability(Build.Capability(tool.Id));
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains($"workers#0.capabilities#1.toolId={tool.Id} is Unrecognized", exception.Message);
	}

	[Fact]
	public void Worker_CapabilityWithNoWorkTime_GetsToolDefault()
	{
		var problem = BasicProblem();
		var tool = Build.Tool(defaultWorkTime: 999);
		var capability = Build.Capability(tool.Id);
		var worker = Build.Worker().WithAddedCapability(capability);

		problem.WithAddedTool(tool).WithWorker(worker).Fill().Validate();

		Assert.Equal(tool.DefaultWorkTime, capability.WorkTime);
	}

	[Fact]
	public void Worker_CapabilityWithNegativeWorkTime_FailsValidation()
	{
		var tool = Build.Tool();
		var capability = Build.Capability(tool.Id, workTime: -1);
		var worker = Build.Worker().WithAddedCapability(capability);
		var problem = BasicProblem().WithWorker(worker).Fill();
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains($"workers#0.capabilities#0.workTime={capability.WorkTime} is LessThanZero", exception.Message);
	}

	[Fact]
	public void Worker_CapabilityWithNoCompletionChance_GetsToolDefault()
	{
		var problem = BasicProblem();
		var tool = Build.Tool(defaultCompletionChance: Random.Shared.NextDouble());
		var capability = Build.Capability(tool.Id);
		var worker = Build.Worker().WithAddedCapability(capability);

		problem.WithAddedTool(tool).WithWorker(worker).Fill().Validate();

		Assert.Equal(tool.DefaultCompletionChance, capability.CompletionChance);
	}

	[Fact]
	public void Worker_CapabilityWithNegativeCompletionChance_FailsValidation()
	{
		var tool = Build.Tool();
		var capability = Build.Capability(tool.Id, completionChance: -1);
		var worker = Build.Worker().WithAddedCapability(capability);
		var problem = BasicProblem().WithWorker(worker).Fill();
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains(
			$"workers#0.capabilities#0.completionChance={capability.CompletionChance} is LessThanZero",
			exception.Message
		);
	}

	[Fact]
	public void CapabilityRewardFactor_WithNonexistentMetric_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		var worker = problem.Workers.First();
		var capability = worker.Capabilities.First();
		var metric = Build.Metric(MetricType.Custom);
		var rewardFactor = Build.RewardFactor(metric.Id);
		capability.RewardFactors.Add(rewardFactor);
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains(
			$"workers#0.capabilities#0.rewardFactors#0.metricId={metric.Id} is Unrecognized",
			exception.Message
		);
	}

	[Fact]
	public void CapabilityRewardFactor_WithDuplicatedMetric_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		var worker = problem.Workers.First();
		var capability = worker.Capabilities.First();
		var rewardFactor = Build.RewardFactor();
		capability.RewardFactors.Add(rewardFactor);
		capability.RewardFactors.Add(rewardFactor);
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains(
			$"workers#0.capabilities#0.rewardFactors#1.metricId={rewardFactor.MetricId} is NotUnique",
			exception.Message
		);
	}

	[Fact]
	public void CapabilityRewardFactor_WithNegativeFactor_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		var worker = problem.Workers.First();
		var capability = worker.Capabilities.First();
		var rewardFactor = Build.RewardFactor(factor: -1);
		capability.RewardFactors.Add(rewardFactor);
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains(
			$"workers#0.capabilities#0.rewardFactors#0.factor={rewardFactor.Factor} is LessThanZero",
			exception.Message
		);
	}

	[Fact]
	public void Metrics_NotProvided_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		problem.Metrics.Clear();
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains($"metrics is MissingOrEmpty", exception.Message);
	}

	[Fact]
	public void Metrics_DuplicateId_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		problem.Metrics.Add(problem.Metrics.First());
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains(
			$"metrics#{problem.Metrics.Count - 1}.id={problem.Metrics[0].Id} is NotUnique",
			exception.Message
		);
	}

	[Theory]
	[InlineData(MetricType.Distance)]
	[InlineData(MetricType.TravelTime)]
	[InlineData(MetricType.WorkTime)]
	public void Metrics_DuplicateBuiltinMetric_FailsValidation(MetricType metricType)
	{
		var problem = BasicProblem().Fill();
		problem.Metrics = [Build.Metric(metricType), Build.Metric(metricType)];
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains($"metrics#1.type={metricType} is NotUnique", exception.Message);
	}

	[Fact]
	public void Metrics_NegativeWeight_FailsValidation()
	{
		var problem = BasicProblem().Fill();
		problem.Metrics = [Build.Metric(MetricType.Distance, weight: -1)];
		var exception = Assert.Throws<ValidationError>(() =>
		{
			problem.Validate();
		});
		Assert.Contains($"metrics#0.weight={problem.Metrics[0].Weight} is LessThanZero", exception.Message);
	}
}