using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Data;
using AsbCloudApp.Data.DailyReport;
using AsbCloudApp.Data.DailyReport.Blocks.Sign;
using AsbCloudApp.Data.DailyReport.Blocks.Subsystems;
using AsbCloudApp.Data.DailyReport.Blocks.TimeBalance;
using AsbCloudApp.Data.DetectedOperation;
using AsbCloudApp.Data.ProcessMaps.Report;
using AsbCloudApp.Data.Subsystems;
using AsbCloudApp.Data.Trajectory;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Requests;
using AsbCloudApp.Services;
using AsbCloudApp.Services.ProcessMaps.WellDrilling;
using AsbCloudApp.Services.Subsystems;
using AsbCloudInfrastructure.Services.DailyReport;
using NSubstitute;
using Xunit;

namespace AsbCloudWebApi.Tests.UnitTests.Services;

public class DailyReportServiceTest
{
	private const int idDailyReport = 1;
	private const int idUser = 3;
	private const int idWell = 2;

	private readonly DateTime dateDailyReport = new DateOnly(2023, 10, 26).ToDateTime(TimeOnly.MinValue);

	private readonly SubsystemBlockDto fakeSubsystemBlock = new()
	{
		IdUser = idUser,
		Wellbore = 999,
		MeasurementsPerDay = 999,
		TotalRopPlan = 999,
		Comment = "Увеличить обороты",
		Subsystems = new[]
		{
			new SubsystemRecordDto
			{
				Name = "АвтоСПО",
				UsagePerDay = new SubsystemParametersDto
				{
					UsedTimeHours = 24,
					SumDepthInterval = 1500,
					KUsage = 15
				},
				UsagePerWell = new SubsystemParametersDto
				{
					UsedTimeHours = 500,
					SumDepthInterval = 3000,
					KUsage = 100
				}
			}
		}
	};

	private readonly SignBlockDto fakeSignBlock = new()
	{
		IdUser = idUser,
		DrillingMaster = new SignRecordDto
		{
			Name = "Иван",
			Patronymic = "Иванович",
			Surname = "Иванов"
		},
		Supervisor = new SignRecordDto()
		{
			Name = "Илья",
			Patronymic = "Ильич",
			Surname = "Бурилов"
		}
	};

	private readonly TimeBalanceBlockDto fakeTimeBalanceBlock = new()
	{
		IdUser = idUser,
		IdSection = 1,
		WellDepth = new PlanFactDto<double?>
		{
			Plan = 2000
		},
		WellOperations = new[]
		{
			new TimeBalanceRecordDto
			{
				IdWellOperation = 1,
				DurationHours = new PlanFactDto<double?>
				{
					Fact = 100,
					Plan = 150,
				},
				DrillingDeviationPerSection = 90,
				DrillingDeviationPerDay = 100,
				ReasonDeviation = "Отклонение"
			}
		}
	};

	private readonly DetectedOperationListDto fakeWellOperationSlipsTime = new()
	{
		Stats = new[]
		{
			new DetectedOperationDrillersStatDto
			{
				Count = 40
			}
		}
	};

	private readonly ProcessMapReportWellDrillingDto fakeProcessMapReportWellDrilling = new()
	{
		DrillingMode = "Ротор",
		DateStart = new DateTime(2023, 10, 26),
		DeltaDepth = 500,
		Rop = new PlanFactDto<double?>
		{
			Plan = 300,
			Fact = 500
		},
		MechDrillingHours = 100
	};

	private readonly WellSectionTypeDto fakeSectionType = new()
	{
		Id = 1,
		Caption = "Пилотный ствол",
	};

	private readonly TrajectoryGeoFactDto fakeLastFactTrajectory = new()
	{
		WellboreDepth = 100,
		VerticalDepth = 150,
		ZenithAngle = 3,
		AzimuthGeo = 5
	};

	private readonly CompanyDto fakeCustomer = new()
	{
		Caption = "Тестовый заказчик",
		IdCompanyType = 1
	};

	private readonly CompanyDto fakeContractor = new()
	{
		Caption = "Тестовый подрядчик",
		IdCompanyType = 2
	};

	private readonly WellOperationDto fakeFirstFactWellOperation = new()
	{
		IdWell = idWell,
		IdParentCategory = 4001,
		IdWellSectionType = 1,
		CategoryName = "Механическое. бурение",
		DateStart = new DateTime(2023, 10, 26),
		DepthStart = 80,
		DepthEnd = 150,
		DurationHours = 8,
	};

	private readonly WellOperationDto fakeLastFactWellOperation = new()
	{
		IdWell = idWell,
		CategoryName = "Механическое. бурение",
		IdWellSectionType = 1,
		IdParentCategory = 4001,
		DateStart = new DateTime(2023, 10, 26),
		DepthStart = 150,
		DepthEnd = 200,
		DurationHours = 8,
	};

	private readonly ScheduleDto fakeShedule = new()
	{
		IdWell = idWell,
		ShiftStart = new TimeDto(1),
		ShiftEnd = new TimeDto(5),
		DrillStart = new DateTime(2023, 01, 26),
		DrillEnd = new DateTime(2023, 12, 26),
		Driller = new()
		{
			Name = "Иван",
			Surname = "Иванов",
			Patronymic = "Бурила"
		}
	};

	private readonly SubsystemStatDto fakeStatSubsystemOperationTime = new()
	{
		SubsystemName = "АПД",
		SumDepthInterval = 250,
		UsedTimeHours = 200,
		KUsage = 30
	};

	private readonly IWellService wellServiceMock = Substitute.For<IWellService>();
	private readonly ITrajectoryEditableRepository<TrajectoryGeoFactDto> trajectoryFactRepositoryMock = Substitute.For<ITrajectoryEditableRepository<TrajectoryGeoFactDto>>();
	private readonly IDailyReportRepository dailyReportRepositoryMock = Substitute.For<IDailyReportRepository>();
	private readonly IScheduleRepository scheduleRepositoryMock = Substitute.For<IScheduleRepository>();
	private readonly IWellOperationRepository wellOperationRepositoryMock = Substitute.For<IWellOperationRepository>();
	private readonly ISubsystemOperationTimeService subsystemOperationTimeServiceMock = Substitute.For<ISubsystemOperationTimeService>();
	private readonly IProcessMapReportWellDrillingService processMapReportWellDrillingServiceMock = Substitute.For<IProcessMapReportWellDrillingService>();
	private readonly IDetectedOperationService detectedOperationServiceMock = Substitute.For<IDetectedOperationService>();

	private readonly DailyReportService dailyReportService;

	private readonly DailyReportDto fakeDailyReport;
	private readonly WellDto fakeWell;

	public DailyReportServiceTest()
	{
		fakeDailyReport = new DailyReportDto
		{
			Id = idDailyReport,
			IdWell = idWell,
			Date = dateDailyReport,
			DateLastUpdate = null
		};

		fakeWell = new WellDto
		{
			Id = idWell,
			Caption = "Тестовое название",
			WellType = "Горизонтальная",
			Cluster = "Тестовый куст",
			Deposit = "Тестовое месторождение",
			Companies = new[] { fakeCustomer, fakeContractor }
		};

		dailyReportService = new DailyReportService(wellServiceMock,
			trajectoryFactRepositoryMock,
			dailyReportRepositoryMock,
			scheduleRepositoryMock,
			wellOperationRepositoryMock,
			subsystemOperationTimeServiceMock,
			processMapReportWellDrillingServiceMock,
			detectedOperationServiceMock);

		dailyReportRepositoryMock.InsertAsync(Arg.Any<DailyReportDto>(), Arg.Any<CancellationToken>())
			.ReturnsForAnyArgs(idDailyReport);

		dailyReportRepositoryMock.GetOrDefaultAsync(Arg.Any<int>(), Arg.Any<DateTime>(), Arg.Any<CancellationToken>())
			.ReturnsForAnyArgs(fakeDailyReport);

		dailyReportRepositoryMock.UpdateAsync(Arg.Any<DailyReportDto>(), Arg.Any<CancellationToken>())
			.ReturnsForAnyArgs(idDailyReport);

		wellServiceMock.GetOrDefaultAsync(Arg.Any<int>(), Arg.Any<CancellationToken>())
			.ReturnsForAnyArgs(fakeWell);

		trajectoryFactRepositoryMock.GetAsync(Arg.Any<TrajectoryRequest>(), Arg.Any<CancellationToken>())
			.ReturnsForAnyArgs(new[] { fakeLastFactTrajectory });

		wellOperationRepositoryMock.GetAsync(Arg.Any<WellOperationRequest>(), Arg.Any<CancellationToken>())
			.ReturnsForAnyArgs(new[] { fakeFirstFactWellOperation, fakeLastFactWellOperation });

		wellOperationRepositoryMock.GetSectionTypes()
			.ReturnsForAnyArgs(new[] { fakeSectionType });

		detectedOperationServiceMock.GetAsync(Arg.Any<DetectedOperationRequest>(), Arg.Any<CancellationToken>())
			.ReturnsForAnyArgs(fakeWellOperationSlipsTime);

		subsystemOperationTimeServiceMock.GetStatAsync(Arg.Any<SubsystemOperationTimeRequest>(), Arg.Any<CancellationToken>())
			.ReturnsForAnyArgs(new[] { fakeStatSubsystemOperationTime });

		scheduleRepositoryMock.GetAsync(idWell, dateDailyReport, Arg.Any<CancellationToken>())
			.ReturnsForAnyArgs(new[] { fakeShedule });

		processMapReportWellDrillingServiceMock.GetAsync(idWell, Arg.Any<CancellationToken>())
			.ReturnsForAnyArgs(new[] { fakeProcessMapReportWellDrilling });
	}

	[Fact]
	public async Task UpdateOrInsertAsync_ShouldReturn_UpdatedSubsystemBlock()
	{
		//act
		var result = await dailyReportService.UpdateOrInsertAsync(idWell, dateDailyReport, idUser, fakeSubsystemBlock, CancellationToken.None);

		//assert
		Assert.NotNull(fakeSubsystemBlock.LastUpdateDate);
		Assert.NotNull(fakeDailyReport.DateLastUpdate);
		Assert.Equal(fakeSubsystemBlock.IdUser, idUser);
		Assert.Equal(fakeDailyReport.SubsystemBlock, fakeSubsystemBlock);
		Assert.Equal(idDailyReport, result);
	}

	[Theory]
	[InlineData("2090.01.01")]
	[InlineData("2000.01.01")]
	public async Task UpdateOrInsertAsync_ShouldReturn_UnableToUpdateDailyReport(DateTime dateDaileReport)
	{
		//act
		var result = await Assert.ThrowsAsync<ArgumentInvalidException>(() => dailyReportService.UpdateOrInsertAsync(
			idWell,
			dateDaileReport, 
			idUser, 
			fakeSignBlock, 
			CancellationToken.None));
		
		//assert
		Assert.Contains("Невозможно обновить суточный отчёт", result.Message);
	}

	[Fact]
	public async Task UpdateOrInsertAsync_ShouldReturn_UpdatedSignBlock()
	{
		//act
		var result = await dailyReportService.UpdateOrInsertAsync(idWell, dateDailyReport, idUser, fakeSignBlock, CancellationToken.None);

		//assert
		Assert.NotNull(fakeSignBlock.LastUpdateDate);
		Assert.NotNull(fakeDailyReport.DateLastUpdate);
		Assert.Equal(fakeSignBlock.IdUser, idUser);
		Assert.Equal(fakeDailyReport.SignBlock, fakeSignBlock);
		Assert.Equal(idDailyReport, result);
	}

	[Fact]
	public async Task UpdateOrInsertAsync_ShouldReturn_UpdatedTimeBalanceBlock()
	{
		//act
		var result = await dailyReportService.UpdateOrInsertAsync(idWell, dateDailyReport, idUser, fakeTimeBalanceBlock,
			CancellationToken.None);

		//assert
		Assert.NotNull(fakeTimeBalanceBlock.LastUpdateDate);
		Assert.NotNull(fakeDailyReport.DateLastUpdate);
		Assert.Equal(fakeTimeBalanceBlock.IdUser, idUser);
		Assert.Equal(fakeDailyReport.TimeBalanceBlock, fakeTimeBalanceBlock);
		Assert.Equal(idDailyReport, result);
	}

	[Theory]
	[InlineData("2090.01.01")]
	[InlineData("2000.01.01")]
	public async Task GetAsync_ShouldReturn_UnableToGetDailyReport(DateTime dateDaileReport)
	{
		//act
		var result = await Assert.ThrowsAsync<ArgumentInvalidException>(() => dailyReportService.GetAsync(idWell,
			dateDaileReport,
			CancellationToken.None));
		
		//assert
		Assert.Contains("Невозможно получить суточный отчёт", result.Message);
	}
	
	[Fact]
	public async Task GetAsync_ShouldReturn_AddedWellInfo()
	{
		//act
		var result = await dailyReportService.GetAsync(idWell, dateDailyReport, CancellationToken.None);

		//assert
		Assert.Equal(result.IdWell, fakeWell.Id);
		Assert.Equal(result.WellCaption, fakeWell.Caption);
		Assert.Equal(result.WellType, fakeWell.WellType);
		Assert.Equal(result.Cluster, fakeWell.Cluster);
		Assert.Equal(result.Deposit, fakeWell.Deposit);
		Assert.Equal(result.Customer, fakeCustomer.Caption);
		Assert.Equal(result.Contractor, fakeContractor.Caption);
		Assert.Equal(result.DepthStart, fakeFirstFactWellOperation.DepthStart);
		Assert.Equal(result.DepthEnd, fakeLastFactWellOperation.DepthEnd);
	}

	[Fact]
	public async Task GetAsync_ShouldReturn_AddedTrajectoryBlock()
	{
		//act
		var result = await dailyReportService.GetAsync(idWell, dateDailyReport, CancellationToken.None);

		//assert
		Assert.Equal(fakeLastFactTrajectory.WellboreDepth, result.TrajectoryBlock.WellboreDepth);
		Assert.Equal(fakeLastFactTrajectory.VerticalDepth, result.TrajectoryBlock.VerticalDepth);
		Assert.Equal(fakeLastFactTrajectory.ZenithAngle, result.TrajectoryBlock.ZenithAngle);
		Assert.Equal(fakeLastFactTrajectory.AzimuthGeo, result.TrajectoryBlock.AzimuthGeo);
	}

	[Fact]
	public async Task GetAsync_ShouldReturn_AddedFactWellOperationBlock()
	{
		//act
		var result = await dailyReportService.GetAsync(idWell, dateDailyReport, CancellationToken.None);

		//assert
		Assert.Equal(16, result.FactWellOperationBlock.SectionDrillingHours);
		Assert.Single(result.FactWellOperationBlock.WellOperations);

		var wellOperation = result.FactWellOperationBlock.WellOperations.Single();
		
		Assert.Equal("Механическое. бурение", wellOperation.CategoryName);
		Assert.Equal(16, wellOperation.DurationHours);
	}

	[Fact]
	public async Task GetAsync_ShouldReturn_AddedScheduleBlock()
	{
		//act
		var result = await dailyReportService.GetAsync(idWell, dateDailyReport, CancellationToken.None);

		//assert
		Assert.Single(result.ScheduleBlock);

		var sheduleRecord = result.ScheduleBlock.Single();
		
		Assert.Equal(fakeShedule.ShiftStart, sheduleRecord.ShiftStart);
		Assert.Equal(fakeShedule.ShiftEnd, sheduleRecord.ShiftEnd);
		Assert.Equal(fakeShedule.Driller?.Name, sheduleRecord.Name);
		Assert.Equal(fakeShedule.Driller?.Surname, sheduleRecord.Surname);
		Assert.Equal(fakeShedule.Driller?.Patronymic, sheduleRecord.Patronymic);
	}

	[Fact]
	public async Task GetAsync_ShouldReturn_UpdatedTimeBalanceBlock()
	{
		//arrange
		fakeDailyReport.TimeBalanceBlock = fakeTimeBalanceBlock;
		
		//act
		var result = await dailyReportService.GetAsync(idWell, dateDailyReport, CancellationToken.None);
		
		//assert
		Assert.NotNull(result.TimeBalanceBlock);
		Assert.Equal(fakeSectionType.Id, result.TimeBalanceBlock.IdSection);
		Assert.Equal(fakeSectionType.Caption, result.TimeBalanceBlock.SectionName);
		Assert.Equal(2000, result.TimeBalanceBlock?.WellDepth.Plan);
		Assert.Equal(120, result.TimeBalanceBlock?.WellDepth.Fact);
		Assert.Equal(40, result.TimeBalanceBlock?.WellOperationSlipsTimeCount);
	}

	[Fact]
	public async Task GetAsync_ShouldReturn_AddedProcessMapWellDrillingBlock()
	{
		//act
		var result = await dailyReportService.GetAsync(idWell, dateDailyReport, CancellationToken.None);

		//assert
		Assert.Single(result.ProcessMapWellDrillingBlock);

		var processMapWellDrillingRecord = result.ProcessMapWellDrillingBlock.Single();

		Assert.Equal(fakeProcessMapReportWellDrilling.DrillingMode, processMapWellDrillingRecord.DrillingMode);
		Assert.Equal(fakeProcessMapReportWellDrilling.Rop.Plan, processMapWellDrillingRecord.Rop.Plan);
		Assert.Equal(fakeProcessMapReportWellDrilling.Rop.Fact, processMapWellDrillingRecord.Rop.Fact);
		Assert.Equal(fakeProcessMapReportWellDrilling.DeltaDepth, processMapWellDrillingRecord.WellBoreDepth);
		Assert.Equal(fakeProcessMapReportWellDrilling.MechDrillingHours, processMapWellDrillingRecord.MechDrillingHours);
	}

	[Fact]
	public async Task GetAsync_ShouldReturn_UpdatedSubsystemBlock()
	{
		//arrange
		fakeDailyReport.SubsystemBlock = fakeSubsystemBlock;

		//act
		var result = await dailyReportService.GetAsync(idDailyReport, dateDailyReport, CancellationToken.None);

		//assert
		Assert.NotNull(result.SubsystemBlock);
		Assert.Equal(2, result.SubsystemBlock?.Subsystems.Count());

		var subsystemRecord0 = result.SubsystemBlock?.Subsystems.ElementAt(0);
		
		Assert.Equal("АвтоСПО", subsystemRecord0?.Name);
		Assert.Equal(24, subsystemRecord0?.UsagePerDay?.UsedTimeHours);
		Assert.Equal(1500, subsystemRecord0?.UsagePerDay?.SumDepthInterval);
		Assert.Equal(15, subsystemRecord0?.UsagePerDay?.KUsage);
		
		Assert.Equal(500, subsystemRecord0?.UsagePerWell?.UsedTimeHours);
		Assert.Equal(3000, subsystemRecord0?.UsagePerWell?.SumDepthInterval);
		Assert.Equal(100, subsystemRecord0?.UsagePerWell?.KUsage);

		var subsystemRecord1 = result.SubsystemBlock?.Subsystems.ElementAt(1);
		
		Assert.Equal("АПД", subsystemRecord1?.Name);
		Assert.Equal(200, subsystemRecord1?.UsagePerDay?.UsedTimeHours);
		Assert.Equal(250, subsystemRecord1?.UsagePerDay?.SumDepthInterval);
		Assert.Equal(30, subsystemRecord1?.UsagePerDay?.KUsage);
		
		Assert.Equal(200, subsystemRecord1?.UsagePerWell?.UsedTimeHours);
		Assert.Equal(250, subsystemRecord1?.UsagePerWell?.SumDepthInterval);
		Assert.Equal(30, subsystemRecord1?.UsagePerWell?.KUsage);
	}

	[Fact]
	public async Task GetAsync_ShouldReturn_FictiveDailyReport()
	{
		//act
		var result = await dailyReportService.GetAsync(idWell, new FileReportRequest(), CancellationToken.None);

		//assert
		Assert.True((fakeLastFactWellOperation.DateStart - fakeFirstFactWellOperation.DateStart).Days == result.Count);
	}

	[Theory]
	[MemberData(nameof(FactWellOperationDates))]
	public async Task GetDatesRangeAsync_ShouldReturn_DateRangeByFactWellOperations(IEnumerable<DateTime> factWellOperationDates)
	{
		//arrange
		wellOperationRepositoryMock.GetAsync(Arg.Any<WellOperationRequest>(), CancellationToken.None)
			.ReturnsForAnyArgs(factWellOperationDates.Select(dateStart => new WellOperationDto { DateStart = dateStart }));

		//act
		var result = await dailyReportService.GetDatesRangeAsync(idWell, CancellationToken.None);

		//assert
		Assert.NotNull(result);
		Assert.True(result.From <= result.To);
		Assert.True(result.To < DateTime.UtcNow.Date);
	}

	public static IEnumerable<object[]> FactWellOperationDates()
	{
		yield return new object[]
		{
			new[]
			{
				new DateTime(2023, 11, 1),
				new DateTime(2023, 11, 9),
				DateTime.UtcNow
			}
		};

		yield return new object[]
		{
			new[]
			{
				new DateTime(2023, 11, 1),
				new DateTime(2023, 11, 1)
			}
		};

		yield return new object[]
		{
			new[]
			{
				DateTime.UtcNow,
				DateTime.UtcNow
			}
		};

		yield return new object[]
		{
			new[]
			{
				new DateTime(2023, 11, 1),
				new DateTime(2023, 11, 9),
				new DateTime(2023, 11, 11)
			}
		};
	}
}