package de.votian.services.service;

import de.votian.services.dto.JobTourServiceDraftRowDto;
import de.votian.services.entity.Job;
import de.votian.services.dto.JobPricingResultDto;
import de.votian.services.entity.ServiceEntity;
import de.votian.services.entity.ServiceType;
import de.votian.services.repository.ServiceEntityRepository;
import de.votian.services.repository.ServiceTypeRepository;
import jakarta.persistence.EntityManager;
import org.junit.jupiter.api.Test;

import java.lang.reflect.Proxy;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

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

class JobTourServiceWriteServiceTest {

    private static final String COUNT_SQL =
            "SELECT COUNT(*) FROM tourservice WHERE jb_id = :jobId";
    private static final String SELECT_ROWS_SQL =
            "SELECT tr_sort, trs_srv_name FROM tourservice WHERE jb_id = :jobId";
    private static final String INSERT_SQL =
            "INSERT INTO tourservice (jb_id, csc_id, tr_sort, srv_id, trs_srv_name, srvt_id, trs_srvt_name, trs_price, trs_discount) " +
                    "VALUES (:jobId, :costCenterId, :tourSort, :serviceId, :serviceName, :serviceTypeId, :serviceTypeName, :price, :discount)";
    private static final String UPDATE_SQL =
            "UPDATE tourservice SET csc_id = :costCenterId, srv_id = :serviceId, srvt_id = :serviceTypeId, trs_srvt_name = :serviceTypeName, trs_price = :price, trs_discount = :discount " +
                    "WHERE jb_id = :jobId AND tr_sort = :tourSort AND trs_srv_name = :serviceName";
    private static final String DELETE_SQL =
            "DELETE FROM tourservice WHERE jb_id = :jobId AND tr_sort = :tourSort AND trs_srv_name = :serviceName";

    @Test
    void createBasePriceRowsCreatesFixAndServicePriceRowsFromJobTotal() {
        List<Map<String, Object>> inserts = new ArrayList<>();
        JobTourServiceWriteService service = new JobTourServiceWriteService(entityManager(0L, List.of(), inserts, new ArrayList<>(), new ArrayList<>()));

        Job job = new Job();
        job.setId(5L);
        job.setCostCenterPayerId(11L);
        job.setTotalPrice(new BigDecimal("105.20"));
        job.setServicePrice(new BigDecimal("7.20"));

        service.createBasePriceRows(job);

        assertThat(inserts).hasSize(2);
        assertThat(inserts)
                .extracting(entry -> entry.get("serviceName"), entry -> entry.get("price"), entry -> entry.get("costCenterId"))
                .containsExactly(
                        org.assertj.core.groups.Tuple.tuple("Fixpreis", new BigDecimal("98.00"), 11L),
                        org.assertj.core.groups.Tuple.tuple("Servicepreis", new BigDecimal("7.20"), 11L)
                );
    }

    @Test
    void createBasePriceRowsFallsBackToSubtotalAndMarkupWhenTotalIsMissing() {
        List<Map<String, Object>> inserts = new ArrayList<>();
        JobTourServiceWriteService service = new JobTourServiceWriteService(entityManager(0L, List.of(), inserts, new ArrayList<>(), new ArrayList<>()));

        Job job = new Job();
        job.setId(6L);
        job.setCostCenterPayerCashId(12L);
        job.setSubtotalPrice(new BigDecimal("80.00"));
        job.setMarkup(new BigDecimal("10.00"));

        service.createBasePriceRows(job);

        assertThat(inserts).singleElement().satisfies(entry -> {
            assertThat(entry.get("serviceName")).isEqualTo("Fixpreis");
            assertThat(entry.get("price")).isEqualTo(new BigDecimal("88.00"));
            assertThat(entry.get("costCenterId")).isEqualTo(12L);
        });
    }

    @Test
    void createBasePriceRowsPrefersCashPayerCostCenterWhenPresent() {
        List<Map<String, Object>> inserts = new ArrayList<>();
        JobTourServiceWriteService service = new JobTourServiceWriteService(entityManager(0L, List.of(), inserts, new ArrayList<>(), new ArrayList<>()));

        Job job = new Job();
        job.setId(61L);
        job.setCostCenterPayerId(11L);
        job.setCostCenterPayerCashId(12L);
        job.setTotalPrice(new BigDecimal("50.00"));

        service.createBasePriceRows(job);

        assertThat(inserts).singleElement().satisfies(entry -> {
            assertThat(entry.get("serviceName")).isEqualTo("Fixpreis");
            assertThat(entry.get("costCenterId")).isEqualTo(12L);
        });
    }

    @Test
    void createBasePriceRowsSkipsJobsWithExistingPersistedRows() {
        List<Map<String, Object>> inserts = new ArrayList<>();
        JobTourServiceWriteService service = new JobTourServiceWriteService(entityManager(2L, List.of(), inserts, new ArrayList<>(), new ArrayList<>()));

        Job job = new Job();
        job.setId(7L);
        job.setCostCenterPayerId(11L);
        job.setTotalPrice(new BigDecimal("50.00"));

        service.createBasePriceRows(job);

        assertThat(inserts).isEmpty();
    }

    @Test
    void createBasePriceRowsFromPricingResultPersistsLegacyFixAndServiceDiscounts() {
        List<Map<String, Object>> inserts = new ArrayList<>();
        JobTourServiceWriteService service = new JobTourServiceWriteService(entityManager(0L, List.of(), inserts, new ArrayList<>(), new ArrayList<>()));

        Job job = new Job();
        job.setId(11L);
        job.setCostCenterPayerId(17L);
        job.setFixPrice(new BigDecimal("100.00"));
        job.setServicePrice(new BigDecimal("10.00"));

        Job recalculated = new Job();
        recalculated.setTotalPrice(new BigDecimal("108.00"));
        recalculated.setServicePrice(new BigDecimal("10.00"));

        JobPricingResultDto result = new JobPricingResultDto();
        result.setJob(recalculated);
        result.setDiscountRate(new BigDecimal("10.00"));
        result.setServicePriceDiscountApplied(true);

        service.createBasePriceRows(job, result);

        assertThat(inserts)
                .extracting(entry -> entry.get("serviceName"), entry -> entry.get("price"), entry -> entry.get("discount"), entry -> entry.get("costCenterId"))
                .containsExactly(
                        org.assertj.core.groups.Tuple.tuple("Fixpreis", new BigDecimal("110.00"), new BigDecimal("10.00"), 17L),
                        org.assertj.core.groups.Tuple.tuple("Servicepreis", new BigDecimal("10.00"), new BigDecimal("10.00"), 17L)
                );
    }

    @Test
    void createBasePriceRowsFromPricingResultKeepsPureServiceRowsWithoutSyntheticFixprice() {
        List<Map<String, Object>> inserts = new ArrayList<>();
        JobTourServiceWriteService service = new JobTourServiceWriteService(entityManager(0L, List.of(), inserts, new ArrayList<>(), new ArrayList<>()));

        Job job = new Job();
        job.setId(12L);
        job.setCostCenterPayerId(18L);
        job.setServicePrice(new BigDecimal("15.00"));

        Job recalculated = new Job();
        recalculated.setTotalPrice(new BigDecimal("13.50"));
        recalculated.setServicePrice(new BigDecimal("15.00"));

        JobPricingResultDto result = new JobPricingResultDto();
        result.setJob(recalculated);
        result.setDiscountRate(new BigDecimal("10.00"));
        result.setServicePriceDiscountApplied(true);

        service.createBasePriceRows(job, result);

        assertThat(inserts)
                .extracting(entry -> entry.get("serviceName"), entry -> entry.get("price"), entry -> entry.get("discount"), entry -> entry.get("costCenterId"))
                .containsExactly(
                        org.assertj.core.groups.Tuple.tuple("Servicepreis", new BigDecimal("15.00"), new BigDecimal("10.00"), 18L)
                );
    }

    @Test
    void createBasePriceRowsFromPricingResultPersistsAutomaticStreetServicePriceAsNetServiceRow() {
        List<Map<String, Object>> inserts = new ArrayList<>();
        JobTourServiceWriteService service = new JobTourServiceWriteService(entityManager(0L, List.of(), inserts, new ArrayList<>(), new ArrayList<>()));

        Job job = new Job();
        job.setId(120L);
        job.setCostCenterPayerId(18L);
        job.setFixPrice(new BigDecimal("100.00"));
        job.setServicePrice(new BigDecimal("15.00"));

        Job recalculated = new Job();
        recalculated.setTotalPrice(new BigDecimal("104.00"));
        recalculated.setServicePrice(new BigDecimal("15.00"));

        JobPricingResultDto result = new JobPricingResultDto();
        result.setJob(recalculated);
        result.setDiscountRate(new BigDecimal("10.00"));
        result.setBaseServicePrice(new BigDecimal("10.00"));
        result.setAutomaticStreetServicePrice(new BigDecimal("5.00"));
        result.setServicePriceDiscountApplied(true);

        service.createBasePriceRows(job, result);

        assertThat(inserts)
                .extracting(entry -> entry.get("serviceName"), entry -> entry.get("price"), entry -> entry.get("discount"), entry -> entry.get("costCenterId"))
                .containsExactly(
                        org.assertj.core.groups.Tuple.tuple("Fixpreis", new BigDecimal("100.00"), new BigDecimal("10.00"), 18L),
                        org.assertj.core.groups.Tuple.tuple("Servicepreis", new BigDecimal("14.00"), new BigDecimal("0.00"), 18L)
                );
    }

    @Test
    void createBasePriceRowsFromPricingResultPersistsRouteRows() {
        List<Map<String, Object>> inserts = new ArrayList<>();
        JobTourServiceWriteService service = new JobTourServiceWriteService(
                entityManager(0L, List.of(), inserts, new ArrayList<>(), new ArrayList<>()),
                serviceEntityRepository(List.of(serviceEntity(71L, "Grundpreis", 1L))),
                serviceTypeRepository(List.of(serviceType(81L, "PKW", 1L)))
        );

        Job job = new Job();
        job.setId(13L);
        job.setHeadquartersId(1L);
        job.setCostCenterPayerId(19L);

        JobPricingResultDto result = new JobPricingResultDto();
        result.setJob(new Job());
        result.setTourServices(List.of(
                row(0, "Grundpreis", "PKW", "21.60", "0.00"),
                row(1, "PLZ 20095 nach PLZ 10115", "", "108.00", "0.00"),
                row(0, "Servicepreis", "", "10.00", "0.00")
        ));

        service.createBasePriceRows(job, result);

        assertThat(inserts)
                .extracting(entry -> entry.get("tourSort"), entry -> entry.get("serviceName"), entry -> entry.get("serviceId"), entry -> entry.get("serviceTypeId"), entry -> entry.get("serviceTypeName"), entry -> entry.get("price"), entry -> entry.get("discount"), entry -> entry.get("costCenterId"))
                .containsExactly(
                        org.assertj.core.groups.Tuple.tuple(0, "Grundpreis", 71L, 81L, "PKW", new BigDecimal("21.60"), new BigDecimal("0.00"), 19L),
                        org.assertj.core.groups.Tuple.tuple(1, "PLZ 20095 nach PLZ 10115", 0L, 0L, "", new BigDecimal("108.00"), new BigDecimal("0.00"), 19L),
                        org.assertj.core.groups.Tuple.tuple(0, "Servicepreis", 0L, 0L, "", new BigDecimal("10.00"), new BigDecimal("0.00"), 19L)
                );
    }

    @Test
    void syncBasePriceRowsUpdatesManagedRowsFromRecalculatedPricing() {
        List<Map<String, Object>> updates = new ArrayList<>();
        JobTourServiceWriteService service = new JobTourServiceWriteService(entityManager(
                2L,
                List.of(new Object[]{0, "Fixpreis"}, new Object[]{0, "Servicepreis"}),
                new ArrayList<>(),
                updates,
                new ArrayList<>()
        ));

        Job job = new Job();
        job.setId(8L);
        job.setCostCenterPayerId(11L);

        Job recalculated = new Job();
        recalculated.setTotalPrice(new BigDecimal("108.00"));
        recalculated.setServicePrice(new BigDecimal("10.00"));

        JobPricingResultDto result = new JobPricingResultDto();
        result.setJob(recalculated);
        result.setDiscountRate(new BigDecimal("10.00"));
        result.setServicePriceDiscountApplied(true);

        service.syncBasePriceRows(job, result);

        assertThat(updates)
                .extracting(entry -> entry.get("serviceName"), entry -> entry.get("price"), entry -> entry.get("discount"), entry -> entry.get("costCenterId"))
                .containsExactly(
                        org.assertj.core.groups.Tuple.tuple("Fixpreis", new BigDecimal("110.00"), new BigDecimal("10.00"), 11L),
                        org.assertj.core.groups.Tuple.tuple("Servicepreis", new BigDecimal("10.00"), new BigDecimal("10.00"), 11L)
                );
    }

    @Test
    void syncBasePriceRowsDeletesServiceRowWhenServicePriceBecomesZero() {
        List<Map<String, Object>> updates = new ArrayList<>();
        List<Map<String, Object>> deletes = new ArrayList<>();
        JobTourServiceWriteService service = new JobTourServiceWriteService(entityManager(
                2L,
                List.of(new Object[]{0, "Fixpreis"}, new Object[]{0, "Servicepreis"}),
                new ArrayList<>(),
                updates,
                deletes
        ));

        Job job = new Job();
        job.setId(9L);
        job.setCostCenterPayerId(11L);

        Job recalculated = new Job();
        recalculated.setTotalPrice(new BigDecimal("55.00"));
        recalculated.setServicePrice(BigDecimal.ZERO);

        JobPricingResultDto result = new JobPricingResultDto();
        result.setJob(recalculated);
        result.setDiscountRate(BigDecimal.ZERO);
        result.setServicePriceDiscountApplied(false);

        service.syncBasePriceRows(job, result);

        assertThat(updates).singleElement().satisfies(entry -> {
            assertThat(entry.get("serviceName")).isEqualTo("Fixpreis");
            assertThat(entry.get("price")).isEqualTo(new BigDecimal("55.00"));
            assertThat(entry.get("discount")).isEqualTo(new BigDecimal("0.00"));
        });
        assertThat(deletes).singleElement().satisfies(entry -> assertThat(entry.get("serviceName")).isEqualTo("Servicepreis"));
    }

    @Test
    void syncBasePriceRowsSkipsJobsWithNonManagedPersistedRows() {
        List<Map<String, Object>> updates = new ArrayList<>();
        List<Map<String, Object>> deletes = new ArrayList<>();
        List<Map<String, Object>> inserts = new ArrayList<>();
        JobTourServiceWriteService service = new JobTourServiceWriteService(entityManager(
                3L,
                List.of(new Object[]{0, "Fixpreis"}, new Object[]{0, "Servicepreis"}, new Object[]{1, "Grundpreis"}),
                inserts,
                updates,
                deletes
        ));

        Job job = new Job();
        job.setId(10L);
        job.setCostCenterPayerId(11L);

        Job recalculated = new Job();
        recalculated.setTotalPrice(new BigDecimal("108.00"));
        recalculated.setServicePrice(new BigDecimal("10.00"));

        JobPricingResultDto result = new JobPricingResultDto();
        result.setJob(recalculated);
        result.setDiscountRate(new BigDecimal("10.00"));
        result.setServicePriceDiscountApplied(true);

        service.syncBasePriceRows(job, result);

        assertThat(inserts).isEmpty();
        assertThat(updates).isEmpty();
        assertThat(deletes).isEmpty();
    }

    @Test
    void syncBasePriceRowsUpdatesManagedRouteRowsAndDeletesRemovedSegments() {
        List<Map<String, Object>> updates = new ArrayList<>();
        List<Map<String, Object>> deletes = new ArrayList<>();
        JobTourServiceWriteService service = new JobTourServiceWriteService(
                entityManager(
                        3L,
                        List.of(
                                new Object[]{0, "Grundpreis"},
                                new Object[]{1, "PLZ 20095 nach PLZ 10115"},
                                new Object[]{2, "PLZ 10115 nach PLZ 50667"}
                        ),
                        new ArrayList<>(),
                        updates,
                        deletes
                ),
                serviceEntityRepository(List.of(serviceEntity(72L, "Grundpreis", 0L))),
                serviceTypeRepository(List.of(serviceType(82L, "PKW", 0L)))
        );

        Job job = new Job();
        job.setId(14L);
        job.setHeadquartersId(1L);
        job.setCostCenterPayerId(20L);

        JobPricingResultDto result = new JobPricingResultDto();
        result.setJob(new Job());
        result.setTourServices(List.of(
                row(0, "Grundpreis", "PKW", "21.60", "0.00"),
                row(1, "PLZ 20095 nach PLZ 10115", "", "108.00", "0.00")
        ));

        service.syncBasePriceRows(job, result);

        assertThat(updates)
                .extracting(entry -> entry.get("tourSort"), entry -> entry.get("serviceName"), entry -> entry.get("serviceId"), entry -> entry.get("serviceTypeId"), entry -> entry.get("serviceTypeName"), entry -> entry.get("price"), entry -> entry.get("discount"), entry -> entry.get("costCenterId"))
                .containsExactly(
                        org.assertj.core.groups.Tuple.tuple(0, "Grundpreis", 72L, 82L, "PKW", new BigDecimal("21.60"), new BigDecimal("0.00"), 20L),
                        org.assertj.core.groups.Tuple.tuple(1, "PLZ 20095 nach PLZ 10115", 0L, 0L, "", new BigDecimal("108.00"), new BigDecimal("0.00"), 20L)
                );
        assertThat(deletes).singleElement().satisfies(entry -> {
            assertThat(entry.get("tourSort")).isEqualTo(2);
            assertThat(entry.get("serviceName")).isEqualTo("PLZ 10115 nach PLZ 50667");
        });
    }

    @Test
    void syncBasePriceRowsPrefersCashPayerCostCenterWhenPresent() {
        List<Map<String, Object>> updates = new ArrayList<>();
        JobTourServiceWriteService service = new JobTourServiceWriteService(entityManager(
                2L,
                List.of(new Object[]{0, "Fixpreis"}, new Object[]{0, "Servicepreis"}),
                new ArrayList<>(),
                updates,
                new ArrayList<>()
        ));

        Job job = new Job();
        job.setId(62L);
        job.setCostCenterPayerId(21L);
        job.setCostCenterPayerCashId(22L);

        Job recalculated = new Job();
        recalculated.setTotalPrice(new BigDecimal("60.00"));
        recalculated.setServicePrice(BigDecimal.ZERO);

        JobPricingResultDto result = new JobPricingResultDto();
        result.setJob(recalculated);
        result.setDiscountRate(BigDecimal.ZERO);
        result.setServicePriceDiscountApplied(false);

        service.syncBasePriceRows(job, result);

        assertThat(updates).singleElement().satisfies(entry -> {
            assertThat(entry.get("serviceName")).isEqualTo("Fixpreis");
            assertThat(entry.get("costCenterId")).isEqualTo(22L);
        });
    }

    private JobTourServiceDraftRowDto row(int tourSort,
                                          String serviceName,
                                          String serviceTypeName,
                                          String price,
                                          String discount) {
        JobTourServiceDraftRowDto row = new JobTourServiceDraftRowDto();
        row.setTourSort(tourSort);
        row.setServiceName(serviceName);
        row.setServiceTypeName(serviceTypeName);
        row.setPrice(new BigDecimal(price));
        row.setDiscount(new BigDecimal(discount));
        return row;
    }

    private ServiceEntityRepository serviceEntityRepository(List<ServiceEntity> byHeadquarters) {
        ServiceEntityRepository repository = mock(ServiceEntityRepository.class);
        when(repository.findByHeadquartersId(0L)).thenReturn(byHeadquarters.stream().filter(service -> Long.valueOf(0L).equals(service.getHeadquartersId())).toList());
        when(repository.findByHeadquartersId(1L)).thenReturn(byHeadquarters.stream().filter(service -> Long.valueOf(1L).equals(service.getHeadquartersId())).toList());
        return repository;
    }

    private ServiceTypeRepository serviceTypeRepository(List<ServiceType> byHeadquarters) {
        ServiceTypeRepository repository = mock(ServiceTypeRepository.class);
        when(repository.findByHeadquartersId(0L)).thenReturn(byHeadquarters.stream().filter(type -> Long.valueOf(0L).equals(type.getHeadquartersId())).toList());
        when(repository.findByHeadquartersId(1L)).thenReturn(byHeadquarters.stream().filter(type -> Long.valueOf(1L).equals(type.getHeadquartersId())).toList());
        return repository;
    }

    private ServiceEntity serviceEntity(Long id, String name, Long headquartersId) {
        ServiceEntity service = new ServiceEntity();
        service.setId(id);
        service.setName(name);
        service.setMode(0);
        service.setHeadquartersId(headquartersId);
        service.setCustomerId(0L);
        return service;
    }

    private ServiceType serviceType(Long id, String name, Long headquartersId) {
        ServiceType serviceType = new ServiceType();
        serviceType.setId(id);
        serviceType.setName(name);
        serviceType.setMode(0);
        serviceType.setHeadquartersId(headquartersId);
        serviceType.setCustomerId(0L);
        return serviceType;
    }

    private EntityManager entityManager(Long count,
                                        List<Object[]> rows,
                                        List<Map<String, Object>> inserts,
                                        List<Map<String, Object>> updates,
                                        List<Map<String, Object>> deletes) {
        return (EntityManager) Proxy.newProxyInstance(
                EntityManager.class.getClassLoader(),
                new Class[]{EntityManager.class},
                (proxy, method, args) -> {
                    if ("createNativeQuery".equals(method.getName())) {
                        String sql = (String) args[0];
                        if (COUNT_SQL.equals(sql)) {
                            return query(count, null, false);
                        }
                        if (SELECT_ROWS_SQL.equals(sql)) {
                            return query(rows, null, false);
                        }
                        if (INSERT_SQL.equals(sql)) {
                            Map<String, Object> parameters = new LinkedHashMap<>();
                            inserts.add(parameters);
                            return query(1, parameters, true);
                        }
                        if (UPDATE_SQL.equals(sql)) {
                            Map<String, Object> parameters = new LinkedHashMap<>();
                            updates.add(parameters);
                            return query(1, parameters, true);
                        }
                        if (DELETE_SQL.equals(sql)) {
                            Map<String, Object> parameters = new LinkedHashMap<>();
                            deletes.add(parameters);
                            return query(1, parameters, true);
                        }
                    }
                    if ("toString".equals(method.getName())) {
                        return "FakeEntityManager";
                    }
                    if ("hashCode".equals(method.getName())) {
                        return System.identityHashCode(proxy);
                    }
                    if ("equals".equals(method.getName())) {
                        return proxy == args[0];
                    }
                    throw new UnsupportedOperationException("Unexpected EntityManager method: " + method.getName());
                }
        );
    }

    private Object query(Object payload, Map<String, Object> parameters, boolean updateQuery) {
        return Proxy.newProxyInstance(
                jakarta.persistence.Query.class.getClassLoader(),
                new Class[]{jakarta.persistence.Query.class},
                (proxy, method, args) -> {
                    if ("setParameter".equals(method.getName())) {
                        if (parameters != null) {
                            parameters.put((String) args[0], args[1]);
                        }
                        return proxy;
                    }
                    if ("getSingleResult".equals(method.getName())) {
                        return payload;
                    }
                    if ("getResultList".equals(method.getName())) {
                        return payload;
                    }
                    if ("executeUpdate".equals(method.getName())) {
                        return updateQuery ? payload : 0;
                    }
                    if ("toString".equals(method.getName())) {
                        return "FakeQuery";
                    }
                    if ("hashCode".equals(method.getName())) {
                        return System.identityHashCode(proxy);
                    }
                    if ("equals".equals(method.getName())) {
                        return proxy == args[0];
                    }
                    throw new UnsupportedOperationException("Unexpected Query method: " + method.getName());
                }
        );
    }
}
