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 AsbCloudDb.Model;
using AsbCloudInfrastructure.Services.DailyReport;
using NSubstitute;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Data.WellOperation;
using Xunit;

namespace AsbCloudWebApi.Tests.Services;

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

    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 ProcessMapReportDataSaubStatDto fakeProcessMapReportWellDrilling = new()
    {
        DrillingMode = "Ротор",
        DateStart = new DateTimeOffset(2023, 10, 26, 0, 0, 0, TimeSpan.Zero),
        DeltaDepth = 500,
        Rop = new PlanFactDto<double?>
        {
            Plan = 300,
            Fact = 500
        },
        DrilledTime = 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,
        OperationCategoryName = "Механическое. бурение",
        DateStart = new DateTime(2023, 10, 26, 0, 0, 0),
        DepthStart = 80,
        DepthEnd = 150,
        DurationHours = 8,
    };

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

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

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

    private readonly SimpleTimezoneDto fakeWellTimezone = new()
    {
        Hours = 5,
    };

    private readonly IWellService wellServiceMock = Substitute.For<IWellService>();
    private readonly ITrajectoryNnbRepository trajectoryFactNnbRepositoryMock = Substitute.For<ITrajectoryNnbRepository>();
    private readonly IDailyReportRepository dailyReportRepositoryMock = Substitute.For<IDailyReportRepository>();
    private readonly IScheduleRepository scheduleRepositoryMock = Substitute.For<IScheduleRepository>();
    private readonly IWellOperationRepository wellOperationRepositoryMock = Substitute.For<IWellOperationRepository>();
    private readonly ISubsystemService subsystemServiceMock = Substitute.For<ISubsystemService>();
    private readonly IProcessMapReportDrillingService processMapReportWellDrillingServiceMock = Substitute.For<IProcessMapReportDrillingService>();
    private readonly IDetectedOperationService detectedOperationServiceMock = Substitute.For<IDetectedOperationService>();

    private readonly DailyReportService dailyReportService;

    private readonly DailyReportDto fakeDailyReport;
    private readonly WellDto fakeWell;
    private readonly DatesRangeDto fakeDatesRange;
    private readonly DataSaubStatRequest fakeRequest = new DataSaubStatRequest();

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

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

        fakeDatesRange = new DatesRangeDto
        {
            From = fakeFirstFactWellOperation.DateStart.DateTime,
            To = fakeLastFactWellOperation.DateStart.DateTime
        };

        dailyReportService = new DailyReportService(wellServiceMock,
            trajectoryFactNnbRepositoryMock,
            dailyReportRepositoryMock,
            scheduleRepositoryMock,
            wellOperationRepositoryMock,
            subsystemServiceMock,
            processMapReportWellDrillingServiceMock,
            detectedOperationServiceMock);

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

        dailyReportRepositoryMock.GetOrDefaultAsync(Arg.Any<int>(), Arg.Any<DateOnly>(), 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);

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

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

        wellOperationRepositoryMock.GetDatesRangeAsync(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .ReturnsForAnyArgs(fakeDatesRange);

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

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

        subsystemServiceMock.GetStatAsync(Arg.Any<SubsystemRequest>(), Arg.Any<CancellationToken>())
            .ReturnsForAnyArgs(new[] { fakeSubsystemsStat });

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

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

        wellServiceMock.GetTimezone(Arg.Any<int>())
            .ReturnsForAnyArgs(fakeWellTimezone);
    }

    [Fact]
    public async Task UpdateOrInsertAsync_ShouldReturn_UpdatedSubsystemBlock()
    {
        //act
        var result = await dailyReportService.UpdateOrInsertAsync(idWell, fakeDailyReport.Date, 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]
    [MemberData(nameof(DateDailyReport))]
    public async Task UpdateOrInsertAsync_ShouldReturn_UnableToUpdateDailyReport(DateOnly dateDailyReport)
    {
        //act
        var result = await Assert.ThrowsAsync<ArgumentInvalidException>(() => dailyReportService.UpdateOrInsertAsync(
            idWell,
            dateDailyReport,
            idUser,
            fakeSignBlock,
            CancellationToken.None));

        //assert
        Assert.Contains("Невозможно обновить суточный отчёт", result.Message);
    }

    [Fact]
    public async Task UpdateOrInsertAsync_ShouldReturn_UpdatedSignBlock()
    {
        //act
        var result = await dailyReportService.UpdateOrInsertAsync(idWell, fakeDailyReport.Date, 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, fakeDailyReport.Date, 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]
    [MemberData(nameof(DateDailyReport))]
    public async Task GetAsync_ShouldReturn_UnableToGetDailyReport(DateOnly dateDailyReport)
    {
        //act
        var result = await Assert.ThrowsAsync<ArgumentInvalidException>(() => dailyReportService.GetAsync(idWell,
            dateDailyReport,
            CancellationToken.None));

        //assert
        Assert.Contains("Невозможно получить суточный отчёт", result.Message);
    }

    [Fact]
    public async Task GetAsync_ShouldReturn_AddedWellInfo()
    {
        //act
        var result = await dailyReportService.GetAsync(idWell, fakeDailyReport.Date, 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, fakeDailyReport.Date, 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, fakeDailyReport.Date, 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, fakeDailyReport.Date, 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, fakeDailyReport.Date, 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, fakeDailyReport.Date, 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.DrilledTime, processMapWellDrillingRecord.MechDrillingHours);
    }

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

        //act
        var result = await dailyReportService.GetAsync(idDailyReport, fakeDailyReport.Date, 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()
    {
        //arrange
        var expectedCount = fakeLastFactWellOperation.DateStart.Day - fakeFirstFactWellOperation.DateStart.Day;

        //act
        var result = await dailyReportService.GetAsync(idWell, new FileReportRequest(), CancellationToken.None);

        //assert
        Assert.Equal(expectedCount, result.Count);
    }

    [Theory]
    [MemberData(nameof(FactWellOperationDatesRange))]
    public async Task GetDatesRangeAsync_ShouldReturn_DateRangeByFactWellOperations(DatesRangeDto datesRange)
    {
        //arrange
        wellOperationRepositoryMock.GetDatesRangeAsync(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns(datesRange);

        //act
        var result = await wellOperationRepositoryMock.GetDatesRangeAsync(idWell, WellOperation.IdOperationTypeFact, CancellationToken.None);

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

    public static IEnumerable<object[]> DateDailyReport()
    {
        yield return new object[]
        {
            new DateOnly(2090, 01, 01),
        };
        yield return new object[]
        {
            new DateOnly(2000, 01, 01)
        };
    }

    public static IEnumerable<object[]> FactWellOperationDatesRange()
    {
        yield return new object[]
        {
            new DatesRangeDto
            {
                From = new DateTime(2023, 11, 1),
                To = new DateTime(2023, 11, 9)
            }
        };

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

        yield return new object[]
        {
            new DatesRangeDto
            {
                From = DateTime.UtcNow,
                To = DateTime.UtcNow
            }
        };

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