package de.assecutor.votianlt.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.catchThrowableOfType;

import de.assecutor.votianlt.model.invoices.CustomerInvoice;
import de.assecutor.votianlt.model.invoices.CustomerInvoiceItem;
import de.assecutor.votianlt.model.invoices.InvoiceType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

/**
 * Pflichtangaben-Validierung nach § 14 UStG. Jeder Test mutiert exakt eine
 * Eigenschaft der Referenzrechnung — so bleibt sichtbar, welche Regel gerade
 * geprüft wird, und die Tests fungieren gleichzeitig als ausführbare
 * Spezifikation.
 */
class InvoiceComplianceValidatorTest {

    private InvoiceComplianceValidator validator;

    @BeforeEach
    void setUp() {
        validator = new InvoiceComplianceValidator();
    }

    @Test
    void acceptsCompleteInvoice() {
        CustomerInvoice invoice = validInvoice();
        validator.validateForIssuance(invoice);
    }

    @Test
    void rejectsMissingInvoiceNumber() {
        CustomerInvoice invoice = validInvoice();
        invoice.setInvoiceNumber("   ");
        assertSingleViolation(invoice, "Rechnungsnummer fehlt");
    }

    @Test
    void rejectsMissingInvoiceDate() {
        CustomerInvoice invoice = validInvoice();
        invoice.setInvoiceDate(null);
        assertSingleViolation(invoice, "Rechnungsdatum");
    }

    @Test
    void rejectsMissingDeliveryDate() {
        CustomerInvoice invoice = validInvoice();
        invoice.setDeliveryDate(null);
        assertSingleViolation(invoice, "Leistungsdatum");
    }

    @Test
    void rejectsMissingSenderName() {
        CustomerInvoice invoice = validInvoice();
        invoice.setSenderName(null);
        assertSingleViolation(invoice, "Name des Leistenden");
    }

    @Test
    void rejectsIncompleteSenderAddress() {
        CustomerInvoice invoice = validInvoice();
        invoice.setSenderPostcode("");
        assertSingleViolation(invoice, "Anschrift des Leistenden");
    }

    @Test
    void rejectsMissingSenderTaxIdentification() {
        CustomerInvoice invoice = validInvoice();
        invoice.setSenderTaxNumber(null);
        invoice.setSenderVatId(null);
        assertSingleViolation(invoice, "Steuernummer oder USt-IdNr");
    }

    @Test
    void acceptsSenderWithOnlyTaxNumber() {
        CustomerInvoice invoice = validInvoice();
        invoice.setSenderVatId(null);
        validator.validateForIssuance(invoice);
    }

    @Test
    void acceptsSenderWithOnlyVatId() {
        CustomerInvoice invoice = validInvoice();
        invoice.setSenderTaxNumber(null);
        validator.validateForIssuance(invoice);
    }

    @Test
    void rejectsMissingRecipientName() {
        CustomerInvoice invoice = validInvoice();
        invoice.setRecipientName(null);
        assertSingleViolation(invoice, "Name des Leistungsempfängers");
    }

    @Test
    void rejectsIncompleteRecipientAddress() {
        CustomerInvoice invoice = validInvoice();
        invoice.setRecipientCity(null);
        assertSingleViolation(invoice, "Anschrift des Leistungsempfängers");
    }

    @Test
    void rejectsEmptyItems() {
        CustomerInvoice invoice = validInvoice();
        invoice.setItems(new ArrayList<>());
        assertSingleViolation(invoice, "Keine Positionen erfasst");
    }

    @Test
    void rejectsItemWithoutDescription() {
        CustomerInvoice invoice = validInvoice();
        invoice.getItems().get(0).setDescription("");
        assertSingleViolation(invoice, "Bezeichnung der Leistung fehlt");
    }

    @Test
    void rejectsItemWithZeroQuantity() {
        CustomerInvoice invoice = validInvoice();
        CustomerInvoiceItem item = invoice.getItems().get(0);
        item.setQuantity(BigDecimal.ZERO);
        // Zwingt netTotal = 0, damit nur die Mengenregel anschlägt und nicht zusätzlich
        // die Konsistenzprüfung der Summen.
        item.setNetTotal(BigDecimal.ZERO);
        invoice.setNetAmount(BigDecimal.ZERO);
        invoice.setVatAmount(BigDecimal.ZERO);
        invoice.setTotalAmount(BigDecimal.ZERO);
        // VAT-Hinweis muss bei 0 % aber gesetzt sein, sonst lösen wir zwei Verstöße aus.
        invoice.setVatRate(new BigDecimal("0.19"));
        assertSingleViolation(invoice, "Menge muss größer 0");
    }

    @Test
    void rejectsItemWithNegativeUnitPrice() {
        CustomerInvoice invoice = validInvoice();
        invoice.getItems().get(0).setUnitPrice(new BigDecimal("-5.00"));
        InvoiceComplianceException ex = catchThrowableOfType(InvoiceComplianceException.class,
                () -> validator.validateForIssuance(invoice));
        assertThat(ex).isNotNull();
        assertThat(ex.getViolations()).anyMatch(v -> v.contains("Einzelpreis"));
    }

    @Test
    void rejectsInconsistentTotals() {
        CustomerInvoice invoice = validInvoice();
        invoice.setTotalAmount(new BigDecimal("999.99"));
        InvoiceComplianceException ex = catchThrowableOfType(InvoiceComplianceException.class,
                () -> validator.validateForIssuance(invoice));
        assertThat(ex).isNotNull();
        assertThat(ex.getViolations()).anyMatch(v -> v.contains("Bruttobetrag passt nicht"));
    }

    @Test
    void rejectsItemSumMismatchingNet() {
        CustomerInvoice invoice = validInvoice();
        // Items summieren sich weiterhin auf 100.00, aber das deklarierte Netto wird verstellt.
        invoice.setNetAmount(new BigDecimal("80.00"));
        invoice.setVatAmount(new BigDecimal("15.20"));
        invoice.setTotalAmount(new BigDecimal("95.20"));
        InvoiceComplianceException ex = catchThrowableOfType(InvoiceComplianceException.class,
                () -> validator.validateForIssuance(invoice));
        assertThat(ex).isNotNull();
        assertThat(ex.getViolations()).anyMatch(v -> v.contains("Summe der Positionen"));
    }

    @Test
    void rejectsMissingVatRate() {
        CustomerInvoice invoice = validInvoice();
        invoice.setVatRate(null);
        assertSingleViolation(invoice, "Steuersatz fehlt");
    }

    @Test
    void rejectsZeroVatWithoutLegalNotice() {
        CustomerInvoice invoice = zeroVatInvoice();
        invoice.setReverseChargeNote(null);
        invoice.setLegalNotes(null);
        assertSingleViolation(invoice, "Bei 0 % USt ist ein rechtlicher Hinweis erforderlich");
    }

    @Test
    void acceptsZeroVatWithReverseChargeNote() {
        CustomerInvoice invoice = zeroVatInvoice();
        invoice.setReverseChargeNote("Steuerschuldnerschaft des Leistungsempfängers (§ 13b UStG).");
        invoice.setLegalNotes(null);
        validator.validateForIssuance(invoice);
    }

    @Test
    void acceptsZeroVatWithLegalNotes() {
        CustomerInvoice invoice = zeroVatInvoice();
        invoice.setReverseChargeNote(null);
        invoice.setLegalNotes("Kleinunternehmer im Sinne des § 19 Abs. 1 UStG — keine Umsatzsteuer ausgewiesen.");
        validator.validateForIssuance(invoice);
    }

    @Test
    void rejectsMismatchingVatAmountForNonZeroRate() {
        CustomerInvoice invoice = validInvoice();
        // Erwartet wären 19,00 € — wir tragen 25,00 € ein.
        invoice.setVatAmount(new BigDecimal("25.00"));
        invoice.setTotalAmount(new BigDecimal("125.00"));
        InvoiceComplianceException ex = catchThrowableOfType(InvoiceComplianceException.class,
                () -> validator.validateForIssuance(invoice));
        assertThat(ex).isNotNull();
        assertThat(ex.getViolations()).anyMatch(v -> v.contains("Steuerbetrag"));
    }

    @Test
    void collectsAllViolationsInOnePass() {
        CustomerInvoice invoice = validInvoice();
        invoice.setInvoiceNumber(null);
        invoice.setSenderName(null);
        invoice.setItems(new ArrayList<>());
        InvoiceComplianceException ex = catchThrowableOfType(InvoiceComplianceException.class,
                () -> validator.validateForIssuance(invoice));
        assertThat(ex).isNotNull();
        assertThat(ex.getViolations())
                .as("Validator soll alle Verstöße sammeln, nicht beim ersten abbrechen")
                .hasSizeGreaterThanOrEqualTo(3);
    }

    @Test
    void cancellationIsNotValidatedHere() {
        CustomerInvoice invoice = validInvoice();
        invoice.setType(InvoiceType.CANCELLATION);
        invoice.setItems(new ArrayList<>()); // wäre für reguläre Rechnung ein Fehler
        validator.validateForIssuance(invoice);
    }

    @Test
    void correctionIsNotValidatedHere() {
        CustomerInvoice invoice = validInvoice();
        invoice.setType(InvoiceType.CORRECTION);
        invoice.setSenderName(null); // wäre für reguläre Rechnung ein Fehler
        validator.validateForIssuance(invoice);
    }

    @Test
    void nullInvoiceIsRejectedDirectly() {
        assertThatThrownBy(() -> validator.validateForIssuance(null))
                .isInstanceOf(IllegalArgumentException.class);
    }

    private void assertSingleViolation(CustomerInvoice invoice, String fragment) {
        InvoiceComplianceException ex = catchThrowableOfType(InvoiceComplianceException.class,
                () -> validator.validateForIssuance(invoice));
        assertThat(ex).as("erwartete InvoiceComplianceException").isNotNull();
        assertThat(ex.getViolations())
                .as("Verstoß mit Fragment '%s' erwartet, war: %s", fragment, ex.getViolations())
                .anyMatch(v -> v.contains(fragment));
    }

    private CustomerInvoice validInvoice() {
        CustomerInvoice invoice = new CustomerInvoice();
        invoice.setType(InvoiceType.INVOICE);
        invoice.setInvoiceNumber("R-2026-0001");
        invoice.setInvoiceDate(LocalDate.of(2026, 5, 3));
        invoice.setDeliveryDate(LocalDate.of(2026, 5, 3));

        invoice.setSenderName("Votianlt Test GmbH");
        invoice.setSenderAddress("Teststraße 1");
        invoice.setSenderPostcode("12345");
        invoice.setSenderCity("Berlin");
        invoice.setSenderCountry("DE");
        invoice.setSenderTaxNumber("12/345/67890");
        invoice.setSenderVatId("DE123456789");

        invoice.setRecipientName("Empfänger AG");
        invoice.setRecipientAddress("Kundenweg 2");
        invoice.setRecipientPostcode("54321");
        invoice.setRecipientCity("Hamburg");
        invoice.setRecipientCountry("DE");

        CustomerInvoiceItem item = new CustomerInvoiceItem(BigDecimal.ONE, "h", "Beratung",
                new BigDecimal("100.00"), new BigDecimal("0.19"));
        invoice.setItems(new ArrayList<>(List.of(item)));

        invoice.setNetAmount(new BigDecimal("100.00"));
        invoice.setVatRate(new BigDecimal("0.19"));
        invoice.setVatAmount(new BigDecimal("19.00"));
        invoice.setTotalAmount(new BigDecimal("119.00"));
        return invoice;
    }

    private CustomerInvoice zeroVatInvoice() {
        CustomerInvoice invoice = validInvoice();
        invoice.setVatRate(BigDecimal.ZERO);
        invoice.setVatAmount(BigDecimal.ZERO);
        invoice.setTotalAmount(invoice.getNetAmount());
        CustomerInvoiceItem item = invoice.getItems().get(0);
        item.setVatRate(BigDecimal.ZERO);
        item.setVatAmount(BigDecimal.ZERO);
        item.setGrossTotal(item.getNetTotal());
        return invoice;
    }
}
