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

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

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

import org.junit.jupiter.api.Test;

import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt;
import de.gecheckt.pdf.umbenenner.application.service.TargetFilenameBuildingService.BaseFilenameReady;
import de.gecheckt.pdf.umbenenner.application.service.TargetFilenameBuildingService.BaseFilenameResult;
import de.gecheckt.pdf.umbenenner.application.service.TargetFilenameBuildingService.InconsistentProposalState;
import de.gecheckt.pdf.umbenenner.domain.model.DateSource;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus;
import de.gecheckt.pdf.umbenenner.domain.model.RunId;

/**
 * Unit tests for {@link TargetFilenameBuildingService}.
 * <p>
 * Covers the verbindliches Zielformat {@code YYYY-MM-DD - Titel.pdf}, the 60-character
 * base-title rule, the fachliche Titelregel (only letters, digits, spaces, and hyphens),
 * Windows-compatibility character removal, and the detection of inconsistent persistence
 * states.
 */
class TargetFilenameBuildingServiceTest {

    private static final DocumentFingerprint FINGERPRINT =
            new DocumentFingerprint("a".repeat(64));
    private static final RunId RUN_ID = new RunId("run-test");
    private static final int TEST_MAX_TITLE_LENGTH = 60;

    // -------------------------------------------------------------------------
    // Null guard
    // -------------------------------------------------------------------------

    @Test
    void buildBaseFilename_rejectsNullAttempt() {
        assertThatNullPointerException()
                .isThrownBy(() -> TargetFilenameBuildingService.buildBaseFilename(null, TEST_MAX_TITLE_LENGTH));
    }

    @Test
    void buildBaseFilename_rejectsZeroMaxTitleLength() {
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 1, 1), "Rechnung");
        org.assertj.core.api.Assertions
                .assertThatIllegalArgumentException()
                .isThrownBy(() -> TargetFilenameBuildingService.buildBaseFilename(attempt, 0))
                .withMessageContaining("maxTitleLength must be >= 1");
    }

    @Test
    void buildBaseFilename_titleExceedsConfiguredMax_reportsConfiguredLimit() {
        String title = "1234567890123456789012345"; // 25 chars
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 1, 1), title);

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, 20);

        assertThat(result).isInstanceOf(InconsistentProposalState.class);
        assertThat(((InconsistentProposalState) result).reason())
                .contains("exceeding 20 characters");
    }

    // -------------------------------------------------------------------------
    // Happy path – correct format
    // -------------------------------------------------------------------------

    @Test
    void buildBaseFilename_validProposal_returnsCorrectFormat() {
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 1, 15), "Rechnung");

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(BaseFilenameReady.class);
        assertThat(((BaseFilenameReady) result).baseFilename())
                .isEqualTo("2026-01-15 - Rechnung.pdf");
    }

    @Test
    void buildBaseFilename_dateWithLeadingZeros_formatsCorrectly() {
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 3, 5), "Kontoauszug");

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(BaseFilenameReady.class);
        assertThat(((BaseFilenameReady) result).baseFilename())
                .isEqualTo("2026-03-05 - Kontoauszug.pdf");
    }

    @Test
    void buildBaseFilename_titleWithDigits_isAccepted() {
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 6, 1), "Rechnung 2026");

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(BaseFilenameReady.class);
        assertThat(((BaseFilenameReady) result).baseFilename())
                .isEqualTo("2026-06-01 - Rechnung 2026.pdf");
    }

    @Test
    void buildBaseFilename_titleWithGermanUmlauts_isAccepted() {
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 4, 7), "Strom Abr");

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(BaseFilenameReady.class);
    }

    @Test
    void buildBaseFilename_titleWithUmlautsAndSzlig_isAccepted() {
        // ä, ö, ü, ß are Unicode letters and must be accepted
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 4, 7), "Büroausgabe");

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(BaseFilenameReady.class);
        assertThat(((BaseFilenameReady) result).baseFilename())
                .isEqualTo("2026-04-07 - Büroausgabe.pdf");
    }

    @Test
    void buildBaseFilename_titleExactly60Chars_isAccepted() {
        String title = "A".repeat(60); // exactly 60 characters
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 1, 1), title);

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(BaseFilenameReady.class);
    }

    // -------------------------------------------------------------------------
    // 60-character rule applies only to base title; format structure is separate
    // -------------------------------------------------------------------------

    @Test
    void buildBaseFilename_format_separatorAndExtensionAreNotCountedAgainstTitle() {
        // A 20-char title produces "YYYY-MM-DD - <20chars>.pdf" — total > 20 chars, which is fine
        String title = "Stromabrechnung 2026"; // 20 chars (well within 60-char limit)
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 3, 31), title);

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(BaseFilenameReady.class);
        String filename = ((BaseFilenameReady) result).baseFilename();
        assertThat(filename).isEqualTo("2026-03-31 - Stromabrechnung 2026.pdf");
        // The service does not append duplicate suffixes; those are added by the target folder adapter
        assertThat(filename).doesNotContain("(");
    }

    // -------------------------------------------------------------------------
    // InconsistentProposalState – null/invalid date
    // -------------------------------------------------------------------------

    @Test
    void buildBaseFilename_nullDate_returnsInconsistentProposalState() {
        ProcessingAttempt attempt = proposalAttempt(null, "Rechnung");

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(InconsistentProposalState.class);
        assertThat(((InconsistentProposalState) result).reason())
                .contains("no resolved date");
    }

    // -------------------------------------------------------------------------
    // InconsistentProposalState – null/blank title
    // -------------------------------------------------------------------------

    @Test
    void buildBaseFilename_nullTitle_returnsInconsistentProposalState() {
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 1, 1), null);

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(InconsistentProposalState.class);
        assertThat(((InconsistentProposalState) result).reason())
                .contains("no validated title");
    }

    @Test
    void buildBaseFilename_blankTitle_returnsInconsistentProposalState() {
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 1, 1), "   ");

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(InconsistentProposalState.class);
        assertThat(((InconsistentProposalState) result).reason())
                .contains("no validated title");
    }

    // -------------------------------------------------------------------------
    // InconsistentProposalState – title exceeds 60 characters
    // -------------------------------------------------------------------------

    @Test
    void buildBaseFilename_titleExceeds60Chars_returnsInconsistentProposalState() {
        String title = "A".repeat(61); // 61 characters
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 1, 1), title);

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(InconsistentProposalState.class);
        assertThat(((InconsistentProposalState) result).reason())
                .contains("exceeding 60 characters");
    }

    // -------------------------------------------------------------------------
    // InconsistentProposalState – disallowed characters in title
    // -------------------------------------------------------------------------

    @Test
    void buildBaseFilename_titleWithHyphen_succeeds() {
        // Hyphens are allowed per fachliche Titelregel
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 1, 1), "Rechnung-2026");

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(BaseFilenameReady.class);
        assertThat(((BaseFilenameReady) result).baseFilename())
                .isEqualTo("2026-01-01 - Rechnung-2026.pdf");
    }

    @Test
    void buildBaseFilename_titleWithSlash_removesWindowsIncompatibleCharacterAndSucceeds() {
        // Slash (/) is a Windows-incompatible character. It should be removed,
        // leaving "RgStrom" which is valid (letters only)
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 1, 1), "Rg/Strom");

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(BaseFilenameReady.class);
        assertThat(((BaseFilenameReady) result).baseFilename())
                .isEqualTo("2026-01-01 - RgStrom.pdf");
    }

    @Test
    void buildBaseFilename_titleWithMultipleWindowsChars_removesAllAndSucceeds() {
        // Multiple Windows-incompatible characters (: and ") should be removed,
        // leaving "Rechnung 2026" which is valid (letters, digits, and spaces)
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 1, 1), "Rechnung: \"2026\"");

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(BaseFilenameReady.class);
        assertThat(((BaseFilenameReady) result).baseFilename())
                .isEqualTo("2026-01-01 - Rechnung 2026.pdf");
    }

    @Test
    void buildBaseFilename_titleWithDot_returnsBaseFilenameReady() {
        // Punkt (.) ist nun ein erlaubtes Zeichen in Titeln
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 1, 1), "Rechnung.Kopie");

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(BaseFilenameReady.class);
        assertThat((BaseFilenameReady) result).extracting("baseFilename")
                .isEqualTo("2026-01-01 - Rechnung.Kopie.pdf");
    }

    // -------------------------------------------------------------------------
    // Windows compatibility – removal of incompatible characters
    // (defensive measure; characters should not appear in validated title)
    // -------------------------------------------------------------------------

    @Test
    void buildBaseFilename_validTitleProducesWindowsCompatibleFilename() {
        // Valid titles containing only letters, digits, spaces, and hyphens should produce
        // correct Windows-compatible filenames
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 5, 20), "Versicherung");

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(BaseFilenameReady.class);
        assertThat(((BaseFilenameReady) result).baseFilename())
                .isEqualTo("2026-05-20 - Versicherung.pdf");
    }

    @Test
    void buildBaseFilename_germanUmlautsAreRetainedInFilename() {
        // German Umlauts (ä, ö, ü) and ß are valid filename characters on Windows
        // and must be retained in the output filename
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 6, 15), "Überprüfung");

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(BaseFilenameReady.class);
        assertThat(((BaseFilenameReady) result).baseFilename())
                .isEqualTo("2026-06-15 - Überprüfung.pdf");
    }

    @Test
    void buildBaseFilename_germanSzligIsRetainedInFilename() {
        // German ß is a valid filename character on Windows and must be retained
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 3, 10), "Straße");

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(BaseFilenameReady.class);
        assertThat(((BaseFilenameReady) result).baseFilename())
                .isEqualTo("2026-03-10 - Straße.pdf");
    }

    @Test
    void buildBaseFilename_completeFormatIsWindowsCompatible() {
        // The complete filename format (YYYY-MM-DD - Titel.pdf) is Windows-compatible
        // The hyphen in the date and the dot in the extension are valid Windows characters
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 12, 31), "Bericht");

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(BaseFilenameReady.class);
        String filename = ((BaseFilenameReady) result).baseFilename();
        assertThat(filename).matches("\\d{4}-\\d{2}-\\d{2} - .+\\.pdf");
        // Verify no Windows-incompatible characters: < > : " / \ | ? *
        assertThat(filename).doesNotContain("<");
        assertThat(filename).doesNotContain(">");
        assertThat(filename).doesNotContain(":");
        assertThat(filename).doesNotContain("\"");
        assertThat(filename).doesNotContain("/");
        assertThat(filename).doesNotContain("\\");
        assertThat(filename).doesNotContain("|");
        assertThat(filename).doesNotContain("?");
        assertThat(filename).doesNotContain("*");
    }

    @Test
    void buildBaseFilename_sixtyCharacterRuleUnaffectedByWindowsCompatibility() {
        // The 60-character rule applies to the base title only.
        // Windows-compatibility cleaning does not change the length counting mechanism.
        String title = "Stromabrechnung 2026"; // 20 characters (within 60-char limit)
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 3, 31), title);

        BaseFilenameResult result = TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(result).isInstanceOf(BaseFilenameReady.class);
        assertThat(((BaseFilenameReady) result).baseFilename())
                .startsWith("2026-03-31 - Stromabrechnung 2026");
    }

    // -------------------------------------------------------------------------
    // InconsistentProposalState reason field is non-null
    // -------------------------------------------------------------------------

    @Test
    void incosistentProposalState_reason_isNeverNull() {
        ProcessingAttempt attempt = proposalAttempt(null, "Rechnung");

        InconsistentProposalState state =
                (InconsistentProposalState) TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(state.reason()).isNotNull();
    }

    // -------------------------------------------------------------------------
    // BaseFilenameReady – result record is non-null and non-blank
    // -------------------------------------------------------------------------

    @Test
    void baseFilenameReady_baseFilename_isNeverNullOrBlank() {
        ProcessingAttempt attempt = proposalAttempt(LocalDate.of(2026, 7, 4), "Bescheid");

        BaseFilenameReady ready =
                (BaseFilenameReady) TargetFilenameBuildingService.buildBaseFilename(attempt, TEST_MAX_TITLE_LENGTH);

        assertThat(ready.baseFilename()).isNotNull().isNotBlank();
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    private ProcessingAttempt proposalAttempt(LocalDate date, String title) {
        return new ProcessingAttempt(
                FINGERPRINT, RUN_ID, 1,
                Instant.now(), Instant.now(),
                ProcessingStatus.PROPOSAL_READY,
                null, null, false,
                "openai-compatible",
                "gpt-4", "prompt-v1.txt", 1, 100,
                "{}", "reasoning text",
                date, DateSource.AI_PROVIDED, title,
                null);
    }
}
