package de.votian.services.service;

import de.votian.services.dto.JobPricingResultDto;
import de.votian.services.entity.GenericDataContainer;
import de.votian.services.entity.Job;
import de.votian.services.entity.JobPrice;
import de.votian.services.entity.Tour;
import de.votian.services.repository.EmployeeRepository;
import de.votian.services.repository.HeadquartersRepository;
import de.votian.services.repository.MetaTypeRepository;
import de.votian.services.repository.ParameterRepository;
import de.votian.services.repository.ServiceEntityRepository;
import de.votian.services.repository.ServiceHistoryRepository;
import de.votian.services.repository.ServicePriceHistoryRepository;
import de.votian.services.repository.ServiceTypeRepository;
import de.votian.services.repository.ServiceZipCodeAreaRepository;
import de.votian.services.repository.ServiceZipCodeAreaAddressRepository;
import de.votian.services.repository.ServiceZipCodeAreaHistoryRepository;
import de.votian.services.repository.ServiceZipCodeAreaMappingRepository;
import de.votian.services.repository.ServiceZipCodeHistoryRepository;
import de.votian.services.repository.ServiceZipCodeRepository;
import de.votian.services.repository.AddressRepository;
import de.votian.services.repository.CostCenterRepository;
import de.votian.services.repository.GenericDataContainerRepository;
import de.votian.services.repository.JobPriceRepository;
import de.votian.services.repository.JobRepository;
import de.votian.services.repository.TourArticleRepository;
import de.votian.services.repository.TourRepository;
import de.votian.services.repository.UserRepository;
import de.votian.services.repository.VehicleDispositionRepository;
import jakarta.persistence.EntityManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

import java.math.BigDecimal;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@SuppressWarnings("null")
class JobServiceTest {

    private JobRepository jobRepository;
    private TourRepository tourRepository;
    private AddressRepository addressRepository;
    private JobPriceRepository jobPriceRepository;
    private GenericDataContainerRepository genericDataContainerRepository;
    private StubJobPricingService jobPricingService;
    private StubJobTourServiceSyncService jobTourServiceSyncService;
    private StubJobTourServiceWriteService jobTourServiceWriteService;
    private JobService jobService;

    @BeforeEach
    void setUp() {
        jobRepository = mock(JobRepository.class);
        tourRepository = mock(TourRepository.class);
        TourArticleRepository tourArticleRepository = mock(TourArticleRepository.class);
        addressRepository = mock(AddressRepository.class);
        jobPriceRepository = mock(JobPriceRepository.class);
        VehicleDispositionRepository vehicleDispositionRepository = mock(VehicleDispositionRepository.class);
        genericDataContainerRepository = mock(GenericDataContainerRepository.class);
        when(genericDataContainerRepository.findByTypeAndIdAndField(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyString()))
                .thenReturn(Optional.empty());
        jobPricingService = new StubJobPricingService();
        jobTourServiceSyncService = new StubJobTourServiceSyncService();
        jobTourServiceWriteService = new StubJobTourServiceWriteService();

        jobService = new JobService(
                jobRepository,
                tourRepository,
                tourArticleRepository,
                addressRepository,
                jobPriceRepository,
                vehicleDispositionRepository,
                genericDataContainerRepository,
                jobPricingService,
                jobTourServiceSyncService,
                jobTourServiceWriteService
        );
    }

    @Test
    void createJobInitializesPersistedTourserviceBaseRows() {
        Job unsaved = new Job();
        unsaved.setCostCenterPayerId(11L);
        unsaved.setTotalPrice(new BigDecimal("25.00"));

        Job saved = new Job();
        saved.setId(5L);
        saved.setCostCenterPayerId(11L);
        saved.setTotalPrice(new BigDecimal("25.00"));
        when(jobRepository.save(unsaved)).thenReturn(saved);

        jobService.createJob(unsaved, List.of(), List.of());

        assertThat(jobTourServiceWriteService.calls).isEqualTo(1);
        assertThat(jobTourServiceWriteService.lastJob).isSameAs(saved);
    }

    @Test
    void createJobAppliesAutomaticStreetServicePriceOnceAndStoresMarker() {
        Job unsaved = new Job();
        unsaved.setHeadquartersId(1L);
        unsaved.setCostCenterPayerId(11L);
        unsaved.setSubtotalPrice(new BigDecimal("100.00"));
        unsaved.setTotalPrice(new BigDecimal("100.00"));

        when(jobRepository.save(org.mockito.ArgumentMatchers.any(Job.class))).thenAnswer(invocation -> {
            Job candidate = invocation.getArgument(0);
            if (candidate.getId() == null) {
                candidate.setId(5L);
            }
            return candidate;
        });
        when(tourRepository.findByJobIdOrderBySort(5L)).thenReturn(List.of());
        jobPricingService.automaticStreetServicePrice = new BigDecimal("5.00");

        Job created = jobService.createJob(unsaved, List.of(), List.of());

        assertThat(created.getId()).isEqualTo(5L);
        assertThat(created.getServicePrice()).isEqualByComparingTo("5.00");
        assertThat(created.getSubtotalPrice()).isEqualByComparingTo("105.00");
        assertThat(created.getTotalPrice()).isEqualByComparingTo("105.00");

        ArgumentCaptor<GenericDataContainer> gdcCaptor = ArgumentCaptor.forClass(GenericDataContainer.class);
        verify(genericDataContainerRepository).save(gdcCaptor.capture());
        GenericDataContainer saved = gdcCaptor.getValue();
        assertThat(saved.getFieldName()).isEqualTo("jb_serviceprice_street_auto");
        assertThat(saved.getContent()).isEqualTo("5.00");
    }

    @Test
    void createJobWithInitialPricesRunsPersistedPricingSyncAtomically() {
        Job unsaved = new Job();
        unsaved.setHeadquartersId(1L);
        unsaved.setCostCenterPayerId(11L);
        unsaved.setMarkup(new BigDecimal("5.00"));
        unsaved.setSubtotalPrice(new BigDecimal("100.00"));
        unsaved.setTotalPrice(new BigDecimal("105.00"));

        when(jobRepository.save(org.mockito.ArgumentMatchers.any(Job.class))).thenAnswer(invocation -> {
            Job candidate = invocation.getArgument(0);
            if (candidate.getId() == null) {
                candidate.setId(5L);
            }
            return candidate;
        });
        when(jobRepository.findById(5L)).thenAnswer(invocation -> Optional.of(unsaved));
        when(tourRepository.findByJobIdOrderBySort(5L)).thenReturn(List.of());

        JobPrice markup = new JobPrice();
        markup.setMetaSort(8);
        markup.setPrice(new BigDecimal("10.00"));
        when(jobPriceRepository.findByJobIdOrderByMetaSortAsc(5L)).thenReturn(List.of(markup));

        JobPricingResultDto pricingResult = new JobPricingResultDto();
        Job recalculatedJob = new Job();
        recalculatedJob.setMarkup(new BigDecimal("10.00"));
        recalculatedJob.setSubtotalPrice(new BigDecimal("88.00"));
        recalculatedJob.setTotalPrice(new BigDecimal("95.20"));
        recalculatedJob.setCourierPrice(new BigDecimal("37.80"));
        recalculatedJob.setCourierSubprice(new BigDecimal("36.00"));
        pricingResult.setJob(recalculatedJob);
        pricingResult.setPrices(List.of(markup));
        jobPricingService.falseResult = pricingResult;

        Job created = jobService.createJob(unsaved, List.of(), List.of(), List.of(markup));

        assertThat(created.getId()).isEqualTo(5L);
        assertThat(created.getMarkup()).isEqualByComparingTo("10.00");
        assertThat(created.getSubtotalPrice()).isEqualByComparingTo("88.00");
        assertThat(created.getTotalPrice()).isEqualByComparingTo("95.20");
        assertThat(jobTourServiceWriteService.calls).isEqualTo(0);
        assertThat(jobTourServiceWriteService.syncCalls).isEqualTo(1);
        assertThat(jobTourServiceSyncService.calls).isEqualTo(1);
        assertThat(jobTourServiceWriteService.lastSyncedJob).isSameAs(unsaved);
        assertThat(jobTourServiceWriteService.lastPricingResult).isSameAs(pricingResult);
    }

    @Test
    void createJobWithExplicitFixAndServicePriceInitializesTourserviceRowsFromPricingResult() {
        Job unsaved = new Job();
        unsaved.setHeadquartersId(1L);
        unsaved.setCostCenterPayerId(11L);
        unsaved.setFixPrice(new BigDecimal("100.00"));
        unsaved.setServicePrice(new BigDecimal("10.00"));

        Job saved = new Job();
        saved.setId(6L);
        saved.setHeadquartersId(1L);
        saved.setCostCenterPayerId(11L);
        saved.setFixPrice(new BigDecimal("100.00"));
        saved.setServicePrice(new BigDecimal("10.00"));
        when(jobRepository.save(unsaved)).thenReturn(saved);

        JobPricingResultDto pricingResult = new JobPricingResultDto();
        Job recalculatedJob = new Job();
        recalculatedJob.setTotalPrice(new BigDecimal("108.00"));
        recalculatedJob.setServicePrice(new BigDecimal("10.00"));
        pricingResult.setJob(recalculatedJob);
        pricingResult.setDiscountRate(new BigDecimal("10.00"));
        pricingResult.setServicePriceDiscountApplied(true);
        jobPricingService.falseResult = pricingResult;

        jobService.createJob(unsaved, List.of(), List.of());

        assertThat(jobTourServiceWriteService.calls).isEqualTo(0);
        assertThat(jobTourServiceWriteService.createWithPricingCalls).isEqualTo(1);
        assertThat(jobTourServiceWriteService.lastCreatedJob).isSameAs(saved);
        assertThat(jobTourServiceWriteService.lastCreatedPricingResult).isSameAs(pricingResult);
    }

    @Test
    void getJobPricesAddsLegacyCourierServicePriceFromGenericDataContainer() {
        JobPrice subtotal = new JobPrice();
        subtotal.setJobId(5L);
        subtotal.setMetaSort(10);
        subtotal.setPrice(new BigDecimal("80.00"));
        when(jobPriceRepository.findByJobIdOrderByMetaSortAsc(5L)).thenReturn(List.of(subtotal));

        GenericDataContainer legacyServicePrice = new GenericDataContainer();
        legacyServicePrice.setObjectType("jb");
        legacyServicePrice.setObjectId(5L);
        legacyServicePrice.setFieldName("jb_cr_serviceprice");
        legacyServicePrice.setContent("7.50");
        when(genericDataContainerRepository.findByTypeAndIdAndField("jb", 5L, "jb_cr_serviceprice"))
                .thenReturn(Optional.of(legacyServicePrice));

        List<JobPrice> result = jobService.getJobPrices(5L);

        assertThat(result)
                .extracting(JobPrice::getMetaSort, JobPrice::getPrice)
                .containsExactly(
                        org.assertj.core.groups.Tuple.tuple(3, new BigDecimal("7.50")),
                        org.assertj.core.groups.Tuple.tuple(10, new BigDecimal("80.00"))
                );
    }

    @Test
    void replaceJobPricesSyncsLegacyCourierServicePriceToGenericDataContainer() {
        when(genericDataContainerRepository.findByTypeAndIdAndField("jb", 5L, "jb_cr_serviceprice"))
                .thenReturn(Optional.empty());
        Job job = new Job();
        job.setId(5L);
        when(jobRepository.findById(5L)).thenReturn(Optional.of(job));
        when(tourRepository.findByJobIdOrderBySort(5L)).thenReturn(List.of());

        JobPrice legacyServicePrice = new JobPrice();
        legacyServicePrice.setMetaSort(3);
        legacyServicePrice.setPrice(new BigDecimal("7.50"));
        JobPrice subtotal = new JobPrice();
        subtotal.setMetaSort(10);
        subtotal.setPrice(new BigDecimal("87.50"));
        when(jobPriceRepository.findByJobIdOrderByMetaSortAsc(5L)).thenReturn(List.of(legacyServicePrice, subtotal));

        JobPricingResultDto pricingResult = new JobPricingResultDto();
        Job recalculatedJob = new Job();
        recalculatedJob.setCourierPrice(new BigDecimal("87.50"));
        recalculatedJob.setCourierSubprice(new BigDecimal("80.00"));
        pricingResult.setJob(recalculatedJob);
        pricingResult.setPrices(List.of(legacyServicePrice, subtotal));
        jobPricingService.falseResult = pricingResult;

        jobService.replaceJobPrices(5L, List.of(legacyServicePrice, subtotal));

        verify(jobPriceRepository, org.mockito.Mockito.times(2)).deleteByJobId(5L);
        ArgumentCaptor<GenericDataContainer> gdcCaptor = ArgumentCaptor.forClass(GenericDataContainer.class);
        verify(genericDataContainerRepository, org.mockito.Mockito.atLeastOnce()).save(gdcCaptor.capture());

        List<GenericDataContainer> savedEntries = gdcCaptor.getAllValues();
        GenericDataContainer saved = savedEntries.get(savedEntries.size() - 1);
        assertThat(saved.getObjectType()).isEqualTo("jb");
        assertThat(saved.getObjectId()).isEqualTo(5L);
        assertThat(saved.getFieldName()).isEqualTo("jb_cr_serviceprice");
        assertThat(saved.getContent()).isEqualTo("7.50");
    }

    @Test
    void replaceJobPricesRecalculatesPersistedCourierFieldsAndDerivedRows() {
        when(genericDataContainerRepository.findByTypeAndIdAndField("jb", 5L, "jb_cr_serviceprice"))
                .thenReturn(Optional.empty());

        Job job = new Job();
        job.setId(5L);
        job.setCourierPrice(new BigDecimal("90.00"));
        when(jobRepository.findById(5L)).thenReturn(Optional.of(job));
        when(tourRepository.findByJobIdOrderBySort(5L)).thenReturn(List.of());

        JobPrice courierMarkup = new JobPrice();
        courierMarkup.setMetaSort(1);
        courierMarkup.setPrice(new BigDecimal("5.00"));
        JobPrice courierProvision = new JobPrice();
        courierProvision.setMetaSort(5);
        courierProvision.setPrice(new BigDecimal("25.00"));
        JobPrice derivedSubtotal = new JobPrice();
        derivedSubtotal.setMetaSort(10);
        derivedSubtotal.setPrice(new BigDecimal("93.75"));
        when(jobPriceRepository.findByJobIdOrderByMetaSortAsc(5L)).thenReturn(List.of(courierMarkup, courierProvision, derivedSubtotal));

        JobPricingResultDto pricingResult = new JobPricingResultDto();
        Job recalculatedJob = new Job();
        recalculatedJob.setCourierPrice(new BigDecimal("93.75"));
        recalculatedJob.setCourierSubprice(new BigDecimal("86.25"));
        pricingResult.setJob(recalculatedJob);
        pricingResult.setPrices(List.of(courierMarkup, courierProvision, derivedSubtotal));

        jobPricingService.falseResult = pricingResult;

        jobService.replaceJobPrices(5L, List.of(courierMarkup, courierProvision));

        ArgumentCaptor<Job> jobCaptor = ArgumentCaptor.forClass(Job.class);
        verify(jobRepository).save(jobCaptor.capture());
        Job savedJob = jobCaptor.getValue();
        assertThat(savedJob.getCourierPrice()).isEqualByComparingTo("93.75");
        assertThat(savedJob.getCourierSubprice()).isEqualByComparingTo("86.25");

        ArgumentCaptor<JobPrice> priceCaptor = ArgumentCaptor.forClass(JobPrice.class);
        verify(jobPriceRepository, org.mockito.Mockito.atLeast(5)).save(priceCaptor.capture());
        assertThat(priceCaptor.getAllValues())
                .extracting(JobPrice::getMetaSort, JobPrice::getPrice)
                .contains(org.assertj.core.groups.Tuple.tuple(10, new BigDecimal("93.75")));
    }

    @Test
    void replaceJobPricesSyncsPersistedCustomerFieldsAndMarksInvoiceTextDirty() {
        when(genericDataContainerRepository.findByTypeAndIdAndField("jb", 5L, "jb_cr_serviceprice"))
                .thenReturn(Optional.empty());
        when(genericDataContainerRepository.findByTypeAndIdAndField("jb", 5L, "jb_invtext_dirty"))
                .thenReturn(Optional.empty());

        Job job = new Job();
        job.setId(5L);
        job.setHeadquartersId(1L);
        job.setMarkup(new BigDecimal("5.00"));
        job.setSubtotalPrice(new BigDecimal("100.00"));
        job.setTotalPrice(new BigDecimal("105.00"));
        when(jobRepository.findById(5L)).thenReturn(Optional.of(job));
        when(tourRepository.findByJobIdOrderBySort(5L)).thenReturn(List.of());

        JobPrice markup = new JobPrice();
        markup.setMetaSort(8);
        markup.setPrice(new BigDecimal("10.00"));
        when(jobPriceRepository.findByJobIdOrderByMetaSortAsc(5L)).thenReturn(List.of(markup));

        JobPricingResultDto pricingResult = new JobPricingResultDto();
        Job recalculatedJob = new Job();
        recalculatedJob.setMarkup(new BigDecimal("10.00"));
        recalculatedJob.setSubtotalPrice(new BigDecimal("88.00"));
        recalculatedJob.setTotalPrice(new BigDecimal("95.20"));
        recalculatedJob.setCourierPrice(new BigDecimal("37.80"));
        recalculatedJob.setCourierSubprice(new BigDecimal("36.00"));
        pricingResult.setJob(recalculatedJob);
        pricingResult.setPrices(List.of(markup));
        jobPricingService.falseResult = pricingResult;

        jobService.replaceJobPrices(5L, List.of(markup));

        ArgumentCaptor<Job> jobCaptor = ArgumentCaptor.forClass(Job.class);
        verify(jobRepository).save(jobCaptor.capture());
        Job savedJob = jobCaptor.getValue();
        assertThat(savedJob.getMarkup()).isEqualByComparingTo("10.00");
        assertThat(savedJob.getSubtotalPrice()).isEqualByComparingTo("88.00");
        assertThat(savedJob.getTotalPrice()).isEqualByComparingTo("95.20");
        assertThat(jobTourServiceSyncService.calls).isEqualTo(1);
        assertThat(jobTourServiceWriteService.syncCalls).isEqualTo(1);
        assertThat(jobTourServiceSyncService.lastJobId).isEqualTo(5L);
        assertThat(jobTourServiceSyncService.lastHeadquartersId).isEqualTo(1L);
        assertThat(jobTourServiceSyncService.lastPreviousMarkup).isEqualByComparingTo("5.00");
        assertThat(jobTourServiceSyncService.lastNextMarkup).isEqualByComparingTo("10.00");
        assertThat(jobTourServiceWriteService.lastSyncedJob).isSameAs(job);
        assertThat(jobTourServiceWriteService.lastPricingResult).isSameAs(pricingResult);

        ArgumentCaptor<GenericDataContainer> gdcCaptor = ArgumentCaptor.forClass(GenericDataContainer.class);
        verify(genericDataContainerRepository).save(gdcCaptor.capture());
        GenericDataContainer savedFlag = gdcCaptor.getValue();
        assertThat(savedFlag.getObjectType()).isEqualTo("jb");
        assertThat(savedFlag.getObjectId()).isEqualTo(5L);
        assertThat(savedFlag.getFieldName()).isEqualTo("jb_invtext_dirty");
        assertThat(savedFlag.getContent()).isEqualTo("1");
        assertThat(savedFlag.getContext()).matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}");
    }

    @Test
    void replaceJobPricesRetriesPersistedSyncWithManualCourierTotalOverride() {
        when(genericDataContainerRepository.findByTypeAndIdAndField("jb", 5L, "jb_cr_serviceprice"))
                .thenReturn(Optional.empty());

        Job job = new Job();
        job.setId(5L);
        job.setCourierPrice(new BigDecimal("63.00"));
        when(jobRepository.findById(5L)).thenReturn(Optional.of(job));
        when(tourRepository.findByJobIdOrderBySort(5L)).thenReturn(List.of());

        JobPrice courierMarkup = new JobPrice();
        courierMarkup.setMetaSort(1);
        courierMarkup.setPrice(new BigDecimal("5.00"));
        when(jobPriceRepository.findByJobIdOrderByMetaSortAsc(5L)).thenReturn(List.of(courierMarkup));

        JobPricingResultDto normalResult = new JobPricingResultDto();
        Job normalJob = new Job();
        normalJob.setCourierPrice(new BigDecimal("55.13"));
        normalResult.setJob(normalJob);
        normalResult.setPrices(List.of(courierMarkup));

        JobPricingResultDto manualResult = new JobPricingResultDto();
        Job manualJob = new Job();
        manualJob.setCourierPrice(new BigDecimal("63.00"));
        manualJob.setCourierSubprice(new BigDecimal("60.00"));
        manualResult.setJob(manualJob);

        JobPrice derivedSubtotal = new JobPrice();
        derivedSubtotal.setMetaSort(10);
        derivedSubtotal.setPrice(new BigDecimal("60.00"));
        manualResult.setPrices(List.of(courierMarkup, derivedSubtotal));

        jobPricingService.falseResult = normalResult;
        jobPricingService.trueResult = manualResult;

        jobService.replaceJobPrices(5L, List.of(courierMarkup));

        assertThat(jobPricingService.manualOverrideCalls).isEqualTo(1);

        ArgumentCaptor<Job> jobCaptor = ArgumentCaptor.forClass(Job.class);
        verify(jobRepository).save(jobCaptor.capture());
        Job savedJob = jobCaptor.getValue();
        assertThat(savedJob.getCourierPrice()).isEqualByComparingTo("63.00");
        assertThat(savedJob.getCourierSubprice()).isEqualByComparingTo("60.00");
    }

    @Test
    void replaceJobPricesUpdatesEmbeddedCourierPriceInPickupRemark() {
        when(genericDataContainerRepository.findByTypeAndIdAndField("jb", 5L, "jb_cr_serviceprice"))
                .thenReturn(Optional.empty());

        Job job = new Job();
        job.setId(5L);
        when(jobRepository.findById(5L)).thenReturn(Optional.of(job));

        Tour pickupTour = new Tour();
        pickupTour.setId(17L);
        pickupTour.setJobId(5L);
        pickupTour.setSort(1);
        pickupTour.setRemark("Hinweis\n[[\nFrachtverguetung: 10,00 EUR\n]]");
        when(tourRepository.findByJobIdOrderBySort(5L)).thenReturn(List.of(pickupTour));
        when(tourRepository.findByJobIdAndSort(5L, 1)).thenReturn(Optional.of(pickupTour));

        JobPrice subtotal = new JobPrice();
        subtotal.setMetaSort(10);
        subtotal.setPrice(new BigDecimal("81.25"));
        when(jobPriceRepository.findByJobIdOrderByMetaSortAsc(5L)).thenReturn(List.of(subtotal));

        JobPricingResultDto pricingResult = new JobPricingResultDto();
        Job recalculatedJob = new Job();
        recalculatedJob.setCourierPrice(new BigDecimal("81.25"));
        recalculatedJob.setCourierSubprice(new BigDecimal("75.00"));
        pricingResult.setJob(recalculatedJob);
        pricingResult.setPrices(List.of(subtotal));
        jobPricingService.falseResult = pricingResult;

        jobService.replaceJobPrices(5L, List.of(subtotal));

        ArgumentCaptor<Tour> tourCaptor = ArgumentCaptor.forClass(Tour.class);
        verify(tourRepository).save(tourCaptor.capture());
        assertThat(tourCaptor.getValue().getRemark())
                .isEqualTo("Hinweis\n[[\nFrachtverguetung: 81,25 EUR\n]]");
    }

    @Test
    void deleteJobPriceRemovesLegacyCourierServicePriceFromGenericDataContainer() {
        GenericDataContainer existing = new GenericDataContainer();
        existing.setId(11L);
        when(genericDataContainerRepository.findByTypeAndIdAndField("jb", 5L, "jb_cr_serviceprice"))
                .thenReturn(Optional.of(existing));
        Job job = new Job();
        job.setId(5L);
        when(jobRepository.findById(5L)).thenReturn(Optional.of(job));
        when(tourRepository.findByJobIdOrderBySort(5L)).thenReturn(List.of());
        when(jobPriceRepository.findByJobIdOrderByMetaSortAsc(5L)).thenReturn(List.of());

        JobPricingResultDto pricingResult = new JobPricingResultDto();
        pricingResult.setJob(new Job());
        pricingResult.setPrices(List.of());
        jobPricingService.falseResult = pricingResult;

        jobService.deleteJobPrice(5L, 3);

        verify(jobPriceRepository).deleteByJobIdAndMetaSort(5L, 3);
        verify(genericDataContainerRepository, org.mockito.Mockito.times(2)).delete(existing);
    }

    private static final class StubJobPricingService extends JobPricingService {

        private JobPricingResultDto falseResult = new JobPricingResultDto();
        private JobPricingResultDto trueResult = new JobPricingResultDto();
        private BigDecimal automaticStreetServicePrice = BigDecimal.ZERO;
        private int manualOverrideCalls;

        private StubJobPricingService() {
            super(
                    mock(CostCenterRepository.class),
                    mock(de.votian.services.repository.CustomerRepository.class),
                    mock(HeadquartersRepository.class),
                    mock(MetaTypeRepository.class),
                    mock(ServiceEntityRepository.class),
                    mock(ServiceTypeRepository.class),
                    mock(ServiceHistoryRepository.class),
                    mock(ServicePriceHistoryRepository.class),
                    mock(ServiceZipCodeRepository.class),
                    mock(ServiceZipCodeHistoryRepository.class),
                    mock(ServiceZipCodeAreaRepository.class),
                    mock(ServiceZipCodeAreaAddressRepository.class),
                    mock(ServiceZipCodeAreaHistoryRepository.class),
                    mock(ServiceZipCodeAreaMappingRepository.class),
                    mock(GenericDataContainerRepository.class),
                    new ParameterService(mock(ParameterRepository.class), mock(EmployeeRepository.class), mock(UserRepository.class))
            );
        }

        @Override
        public JobPricingResultDto calculateDraft(Job draft,
                                                  List<JobPrice> inputPrices,
                                                  List<de.votian.services.dto.TourDetailDto> tours,
                                                  boolean manualCourierTotalOverride) {
            if (manualCourierTotalOverride) {
                manualOverrideCalls++;
                return trueResult;
            }
            return falseResult;
        }

        @Override
        public BigDecimal calculateAutomaticStreetServicePrice(Job job, List<de.votian.services.dto.TourDetailDto> tours) {
            return automaticStreetServicePrice;
        }
    }

    private static final class StubJobTourServiceSyncService extends JobTourServiceSyncService {

        private Long lastJobId;
        private Long lastHeadquartersId;
        private BigDecimal lastPreviousMarkup;
        private BigDecimal lastNextMarkup;
        private int calls;

        private StubJobTourServiceSyncService() {
            super(
                    new ParameterService(mock(ParameterRepository.class), mock(EmployeeRepository.class), mock(UserRepository.class)),
                    unsupportedEntityManager()
            );
        }

        @Override
        public void syncMarkupAdjustedPrices(Long jobId,
                                             Long headquartersId,
                                             BigDecimal previousMarkup,
                                             BigDecimal nextMarkup) {
            calls++;
            lastJobId = jobId;
            lastHeadquartersId = headquartersId;
            lastPreviousMarkup = previousMarkup;
            lastNextMarkup = nextMarkup;
        }
    }

    private static final class StubJobTourServiceWriteService extends JobTourServiceWriteService {

        private int calls;
        private int createWithPricingCalls;
        private int syncCalls;
        private Job lastJob;
        private Job lastCreatedJob;
        private Job lastSyncedJob;
        private JobPricingResultDto lastCreatedPricingResult;
        private JobPricingResultDto lastPricingResult;

        private StubJobTourServiceWriteService() {
            super(unsupportedEntityManager());
        }

        @Override
        public void createBasePriceRows(Job job) {
            calls++;
            lastJob = job;
        }

        @Override
        public void createBasePriceRows(Job job, JobPricingResultDto pricingResult) {
            createWithPricingCalls++;
            lastCreatedJob = job;
            lastCreatedPricingResult = pricingResult;
        }

        @Override
        public void syncBasePriceRows(Job job, JobPricingResultDto pricingResult) {
            syncCalls++;
            lastSyncedJob = job;
            lastPricingResult = pricingResult;
        }
    }

    private static EntityManager unsupportedEntityManager() {
        return (EntityManager) Proxy.newProxyInstance(
                EntityManager.class.getClassLoader(),
                new Class[]{EntityManager.class},
                (proxy, method, args) -> {
                    if ("toString".equals(method.getName())) {
                        return "UnsupportedEntityManager";
                    }
                    if ("hashCode".equals(method.getName())) {
                        return System.identityHashCode(proxy);
                    }
                    if ("equals".equals(method.getName())) {
                        return proxy == args[0];
                    }
                    throw new UnsupportedOperationException("EntityManager should not be used in this test.");
                }
        );
    }
}
