package de.assecutor.votianlt.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import de.assecutor.votianlt.model.invoices.CustomerInvoice;
import de.assecutor.votianlt.model.invoices.InvoiceNumberReservation;
import de.assecutor.votianlt.model.invoices.InvoiceNumberReservationStatus;
import de.assecutor.votianlt.repository.InvoiceNumberReservationRepository;
import org.bson.types.ObjectId;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.List;
import java.util.Optional;

@ExtendWith(MockitoExtension.class)
class InvoiceNumberAuditServiceTest {

    @Mock
    private InvoiceNumberReservationRepository repository;

    private InvoiceNumberAuditService service;

    private final ObjectId userId = new ObjectId();

    @BeforeEach
    void setUp() {
        service = new InvoiceNumberAuditService(repository);
    }

    @Test
    void markUsedTransitionsExistingReservation() {
        InvoiceNumberReservation reservation = reservation("R-2026-000010", 10L,
                InvoiceNumberReservationStatus.RESERVED);
        when(repository.findByUserIdAndNumber(userId, "R-2026-000010")).thenReturn(Optional.of(reservation));
        when(repository.save(any(InvoiceNumberReservation.class))).thenAnswer(inv -> inv.getArgument(0));

        CustomerInvoice invoice = invoice("R-2026-000010", "invoice-id-42");
        service.markUsed(invoice);

        ArgumentCaptor<InvoiceNumberReservation> captor = ArgumentCaptor.forClass(InvoiceNumberReservation.class);
        verify(repository).save(captor.capture());
        InvoiceNumberReservation saved = captor.getValue();
        assertThat(saved.getStatus()).isEqualTo(InvoiceNumberReservationStatus.USED);
        assertThat(saved.getInvoiceId()).isEqualTo("invoice-id-42");
        assertThat(saved.getUsedAt()).isNotNull();
    }

    @Test
    void markUsedBootstrapsReservationForLegacyInvoiceWithoutPriorReservation() {
        when(repository.findByUserIdAndNumber(userId, "RE-2024-0007")).thenReturn(Optional.empty());
        when(repository.save(any(InvoiceNumberReservation.class))).thenAnswer(inv -> inv.getArgument(0));

        service.markUsed(invoice("RE-2024-0007", "legacy-invoice"));

        ArgumentCaptor<InvoiceNumberReservation> captor = ArgumentCaptor.forClass(InvoiceNumberReservation.class);
        verify(repository).save(captor.capture());
        InvoiceNumberReservation saved = captor.getValue();
        assertThat(saved.getStatus()).isEqualTo(InvoiceNumberReservationStatus.USED);
        assertThat(saved.getNumber()).isEqualTo("RE-2024-0007");
        assertThat(saved.getSequence()).isEqualTo(7L);
        assertThat(saved.getReservedBy()).contains("bootstrap");
    }

    @Test
    void markUsedSwallowsRepositoryFailures() {
        when(repository.findByUserIdAndNumber(any(), any())).thenThrow(new RuntimeException("Mongo down"));
        // Erwartung: keine Exception nach außen — Festschreiben darf an Audit nicht scheitern.
        service.markUsed(invoice("R-2026-1", "i-1"));
    }

    @Test
    void markUsedIgnoresInvoiceWithoutNumberOrUserId() {
        CustomerInvoice missingNumber = new CustomerInvoice();
        missingNumber.setUserId(userId.toHexString());
        service.markUsed(missingNumber);

        CustomerInvoice missingUser = new CustomerInvoice();
        missingUser.setInvoiceNumber("R-1");
        service.markUsed(missingUser);

        verify(repository, never()).findByUserIdAndNumber(any(), any());
        verify(repository, never()).save(any());
    }

    @Test
    void markVoidedRequiresReason() {
        assertThatThrownBy(() -> service.markVoided(userId, "R-1", "  "))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("Grund");
        assertThatThrownBy(() -> service.markVoided(userId, "R-1", null))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void markVoidedTransitionsReservedToVoided() {
        InvoiceNumberReservation reservation = reservation("R-2026-000005", 5L,
                InvoiceNumberReservationStatus.RESERVED);
        when(repository.findByUserIdAndNumber(userId, "R-2026-000005")).thenReturn(Optional.of(reservation));
        when(repository.save(any(InvoiceNumberReservation.class))).thenAnswer(inv -> inv.getArgument(0));

        service.markVoided(userId, "R-2026-000005", "Versehentlich vergeben, Kunde widerrufen");

        ArgumentCaptor<InvoiceNumberReservation> captor = ArgumentCaptor.forClass(InvoiceNumberReservation.class);
        verify(repository).save(captor.capture());
        InvoiceNumberReservation saved = captor.getValue();
        assertThat(saved.getStatus()).isEqualTo(InvoiceNumberReservationStatus.VOIDED);
        assertThat(saved.getVoidReason()).isEqualTo("Versehentlich vergeben, Kunde widerrufen");
        assertThat(saved.getVoidedAt()).isNotNull();
    }

    @Test
    void markVoidedRefusesToOverwriteUsedReservation() {
        InvoiceNumberReservation reservation = reservation("R-2026-000005", 5L,
                InvoiceNumberReservationStatus.USED);
        when(repository.findByUserIdAndNumber(userId, "R-2026-000005")).thenReturn(Optional.of(reservation));

        assertThatThrownBy(() -> service.markVoided(userId, "R-2026-000005", "Test"))
                .isInstanceOf(InvoiceLifecycleException.class)
                .hasMessageContaining("ausgestellten Rechnung");
    }

    @Test
    void markVoidedFailsWhenNoReservationFound() {
        when(repository.findByUserIdAndNumber(userId, "R-NOPE")).thenReturn(Optional.empty());

        assertThatThrownBy(() -> service.markVoided(userId, "R-NOPE", "irrelevant"))
                .isInstanceOf(InvoiceLifecycleException.class)
                .hasMessageContaining("Keine Reservierung");
    }

    @Test
    void findUnusedReturnsOnlyReservedAndVoided() {
        when(repository.findByUserIdOrderBySequenceAsc(userId)).thenReturn(List.of(
                reservation("R-1", 1L, InvoiceNumberReservationStatus.USED),
                reservation("R-2", 2L, InvoiceNumberReservationStatus.RESERVED),
                reservation("R-3", 3L, InvoiceNumberReservationStatus.USED),
                reservation("R-4", 4L, InvoiceNumberReservationStatus.VOIDED)));

        List<InvoiceNumberReservation> unused = service.findUnused(userId);

        assertThat(unused).extracting(InvoiceNumberReservation::getNumber).containsExactly("R-2", "R-4");
    }

    private CustomerInvoice invoice(String number, String invoiceId) {
        CustomerInvoice invoice = new CustomerInvoice();
        invoice.setId(invoiceId);
        invoice.setInvoiceNumber(number);
        invoice.setUserId(userId.toHexString());
        return invoice;
    }

    private InvoiceNumberReservation reservation(String number, long sequence,
            InvoiceNumberReservationStatus status) {
        InvoiceNumberReservation reservation = new InvoiceNumberReservation();
        reservation.setUserId(userId);
        reservation.setNumber(number);
        reservation.setSequence(sequence);
        reservation.setStatus(status);
        return reservation;
    }
}
