package de.gecheckt.pdf.umbenenner.application.service;

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

import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import de.gecheckt.pdf.umbenenner.application.port.out.ClockPort;
import de.gecheckt.pdf.umbenenner.domain.model.DateSource;
import de.gecheckt.pdf.umbenenner.domain.model.NamingProposal;
import de.gecheckt.pdf.umbenenner.domain.model.ParsedAiResponse;

/**
 * Unit tests for {@link AiResponseValidator}.
 * <p>
 * Covers: title character rules, length limit, generic placeholder detection,
 * date parsing, date fallback via {@link ClockPort}, and null handling.
 */
class AiResponseValidatorTest {

    private static final Instant FIXED_INSTANT = Instant.parse("2026-04-07T10:00:00Z");
    private static final LocalDate FIXED_DATE = FIXED_INSTANT.atZone(ZoneOffset.UTC).toLocalDate();
    private static final int TEST_MAX_TITLE_LENGTH = 60;

    private AiResponseValidator validator;

    @BeforeEach
    void setUp() {
        ClockPort fixedClock = () -> FIXED_INSTANT;
        validator = new AiResponseValidator(fixedClock, TEST_MAX_TITLE_LENGTH);
    }

    // -------------------------------------------------------------------------
    // Valid cases
    // -------------------------------------------------------------------------

    @Test
    void validate_validTitleAndAiDate_returnsValidWithAiProvided() {
        ParsedAiResponse parsed = ParsedAiResponse.of("Stromabrechnung", "Electricity bill", "2026-01-15");

        AiResponseValidator.AiValidationResult result = validator.validate(parsed);

        assertThat(result).isInstanceOf(AiResponseValidator.AiValidationResult.Valid.class);
        NamingProposal proposal = ((AiResponseValidator.AiValidationResult.Valid) result).proposal();
        assertThat(proposal.validatedTitle()).isEqualTo("Stromabrechnung");
        assertThat(proposal.resolvedDate()).isEqualTo(LocalDate.of(2026, 1, 15));
        assertThat(proposal.dateSource()).isEqualTo(DateSource.AI_PROVIDED);
        assertThat(proposal.aiReasoning()).isEqualTo("Electricity bill");
    }

    @Test
    void validate_validTitleNoDate_usesFallbackCurrentDate() {
        ParsedAiResponse parsed = ParsedAiResponse.of("Kontoauszug", "No date in document", null);

        AiResponseValidator.AiValidationResult result = validator.validate(parsed);

        assertThat(result).isInstanceOf(AiResponseValidator.AiValidationResult.Valid.class);
        NamingProposal proposal = ((AiResponseValidator.AiValidationResult.Valid) result).proposal();
        assertThat(proposal.resolvedDate()).isEqualTo(FIXED_DATE);
        assertThat(proposal.dateSource()).isEqualTo(DateSource.FALLBACK_CURRENT);
    }

    @Test
    void validate_aiProvidesEmptyDateString_usesFallbackCurrentDate() {
        ParsedAiResponse parsed = ParsedAiResponse.of("Kontoauszug", "No date in document", "");

        AiResponseValidator.AiValidationResult result = validator.validate(parsed);

        assertThat(result).isInstanceOf(AiResponseValidator.AiValidationResult.Valid.class);
        NamingProposal proposal = ((AiResponseValidator.AiValidationResult.Valid) result).proposal();
        assertThat(proposal.resolvedDate()).isEqualTo(FIXED_DATE);
        assertThat(proposal.dateSource()).isEqualTo(DateSource.FALLBACK_CURRENT);
    }

    @Test
    void validate_titleWithUmlauts_isAccepted() {
        ParsedAiResponse parsed = ParsedAiResponse.of("Mietvertrag Müller", "Rental contract", null);

        AiResponseValidator.AiValidationResult result = validator.validate(parsed);

        assertThat(result).isInstanceOf(AiResponseValidator.AiValidationResult.Valid.class);
    }

    @Test
    void validate_titleWithSzligChar_isAccepted() {
        ParsedAiResponse parsed = ParsedAiResponse.of("Straßenrechnung", "Street bill", null);

        AiResponseValidator.AiValidationResult result = validator.validate(parsed);

        assertThat(result).isInstanceOf(AiResponseValidator.AiValidationResult.Valid.class);
    }

    @Test
    void validate_titleWithDigits_isAccepted() {
        ParsedAiResponse parsed = ParsedAiResponse.of("Rechnung 2026", "Invoice 2026", null);

        AiResponseValidator.AiValidationResult result = validator.validate(parsed);

        assertThat(result).isInstanceOf(AiResponseValidator.AiValidationResult.Valid.class);
    }

    @Test
    void validate_titleExactly20Chars_isAccepted() {
        String title = "12345678901234567890"; // exactly 20 chars
        ParsedAiResponse parsed = ParsedAiResponse.of(title, "test", null);

        AiResponseValidator.AiValidationResult result = validator.validate(parsed);

        assertThat(result).isInstanceOf(AiResponseValidator.AiValidationResult.Valid.class);
    }

    @Test
    void validate_emptyReasoning_isAccepted() {
        ParsedAiResponse parsed = ParsedAiResponse.of("Rechnung", "", null);

        AiResponseValidator.AiValidationResult result = validator.validate(parsed);

        assertThat(result).isInstanceOf(AiResponseValidator.AiValidationResult.Valid.class);
    }

    // -------------------------------------------------------------------------
    // Title validation failures
    // -------------------------------------------------------------------------

    @Test
    void validate_titleOver60Chars_returnsInvalid() {
        String title = "1234567890123456789012345678901234567890123456789012345678901"; // 61 chars
        ParsedAiResponse parsed = ParsedAiResponse.of(title, "reasoning", null);

        AiResponseValidator.AiValidationResult result = validator.validate(parsed);

        assertThat(result).isInstanceOf(AiResponseValidator.AiValidationResult.Invalid.class);
        assertThat(((AiResponseValidator.AiValidationResult.Invalid) result).errorMessage())
                .contains("60");
    }

    @Test
    void validate_titleWithSpecialChar_returnsInvalid() {
        ParsedAiResponse parsed = ParsedAiResponse.of("Rechnung!", "reasoning", null);

        AiResponseValidator.AiValidationResult result = validator.validate(parsed);

        assertThat(result).isInstanceOf(AiResponseValidator.AiValidationResult.Invalid.class);
        assertThat(((AiResponseValidator.AiValidationResult.Invalid) result).errorMessage())
                .containsIgnoringCase("disallowed");
    }

    @Test
    void validate_titleWithHyphen_isAccepted() {
        ParsedAiResponse parsed = ParsedAiResponse.of("Strom-Rechnung", "reasoning", null);

        AiResponseValidator.AiValidationResult result = validator.validate(parsed);

        assertThat(result).isInstanceOf(AiResponseValidator.AiValidationResult.Valid.class);
    }

    @Test
    void validate_genericTitleDokument_returnsInvalid() {
        ParsedAiResponse parsed = ParsedAiResponse.of("Dokument", "reasoning", null);

        AiResponseValidator.AiValidationResult result = validator.validate(parsed);

        assertThat(result).isInstanceOf(AiResponseValidator.AiValidationResult.Invalid.class);
        assertThat(((AiResponseValidator.AiValidationResult.Invalid) result).errorMessage())
                .containsIgnoringCase("placeholder");
    }

    @Test
    void validate_genericTitleDateiCaseInsensitive_returnsInvalid() {
        ParsedAiResponse parsed = ParsedAiResponse.of("DATEI", "reasoning", null);

        AiResponseValidator.AiValidationResult result = validator.validate(parsed);

        assertThat(result).isInstanceOf(AiResponseValidator.AiValidationResult.Invalid.class);
    }

    @Test
    void validate_genericTitleScan_returnsInvalid() {
        ParsedAiResponse parsed = ParsedAiResponse.of("scan", "reasoning", null);

        AiResponseValidator.AiValidationResult result = validator.validate(parsed);

        assertThat(result).isInstanceOf(AiResponseValidator.AiValidationResult.Invalid.class);
    }

    @Test
    void validate_genericTitlePdf_returnsInvalid() {
        ParsedAiResponse parsed = ParsedAiResponse.of("PDF", "reasoning", null);

        AiResponseValidator.AiValidationResult result = validator.validate(parsed);

        assertThat(result).isInstanceOf(AiResponseValidator.AiValidationResult.Invalid.class);
    }

    // -------------------------------------------------------------------------
    // Date validation failures
    // -------------------------------------------------------------------------

    @Test
    void validate_aiProvidesUnparseableDate_returnsInvalid() {
        ParsedAiResponse parsed = ParsedAiResponse.of("Rechnung", "reasoning", "not-a-date");

        AiResponseValidator.AiValidationResult result = validator.validate(parsed);

        assertThat(result).isInstanceOf(AiResponseValidator.AiValidationResult.Invalid.class);
        assertThat(((AiResponseValidator.AiValidationResult.Invalid) result).errorMessage())
                .contains("not-a-date");
    }

    @Test
    void validate_aiProvidesWrongDateFormat_returnsInvalid() {
        ParsedAiResponse parsed = ParsedAiResponse.of("Rechnung", "reasoning", "15.01.2026");

        AiResponseValidator.AiValidationResult result = validator.validate(parsed);

        assertThat(result).isInstanceOf(AiResponseValidator.AiValidationResult.Invalid.class);
    }

    @Test
    void validate_aiProvidesPartialDate_returnsInvalid() {
        ParsedAiResponse parsed = ParsedAiResponse.of("Rechnung", "reasoning", "2026-01");

        AiResponseValidator.AiValidationResult result = validator.validate(parsed);

        assertThat(result).isInstanceOf(AiResponseValidator.AiValidationResult.Invalid.class);
    }

    // -------------------------------------------------------------------------
    // Null handling
    // -------------------------------------------------------------------------

    @Test
    void validate_nullParsedResponse_throwsNullPointerException() {
        assertThatThrownBy(() -> validator.validate(null))
                .isInstanceOf(NullPointerException.class)
                .hasMessage("parsed must not be null");
    }

    @Test
    void constructor_nullClockPort_throwsNullPointerException() {
        assertThatThrownBy(() -> new AiResponseValidator(null, TEST_MAX_TITLE_LENGTH))
                .isInstanceOf(NullPointerException.class)
                .hasMessage("clockPort must not be null");
    }

    @Test
    void constructor_zeroMaxTitleLength_throwsIllegalArgumentException() {
        ClockPort fixedClock = () -> FIXED_INSTANT;
        assertThatThrownBy(() -> new AiResponseValidator(fixedClock, 0))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("maxTitleLength must be >= 1");
    }

    @Test
    void validate_titleExceedsConfiguredMax_returnsInvalidWithConfiguredLimitInMessage() {
        ClockPort fixedClock = () -> FIXED_INSTANT;
        AiResponseValidator strictValidator = new AiResponseValidator(fixedClock, 20);
        String title = "1234567890123456789012345"; // 25 chars > 20
        ParsedAiResponse parsed = ParsedAiResponse.of(title, "reasoning", null);

        AiResponseValidator.AiValidationResult result = strictValidator.validate(parsed);

        assertThat(result).isInstanceOf(AiResponseValidator.AiValidationResult.Invalid.class);
        assertThat(((AiResponseValidator.AiValidationResult.Invalid) result).errorMessage())
                .contains("20");
    }
}
