package org.raddatz.familienarchiv.document;

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

import java.util.concurrent.TimeUnit;

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

/**
 * The backfill overwrite heuristic (FR-004) in isolation — every emittable date-label form is
 * recognised, prose is left alone, and a regex-metacharacter index is matched literally without
 * hanging. The exact label spellings mirror {@code docs/date-label-fixtures.json}.
 */
class DocumentTitleBackfillMatcherTest {

    private static boolean overwritable(String title, String location) {
        return DocumentTitleBackfillMatcher.isOverwritable(title, "C-0029", location);
    }

    // ─── each date-label form (index + form) is overwritable ──────────────────

    @Test
    void year_form() {
        assertThat(overwritable("C-0029 – 1916", null)).isTrue();
    }

    @Test
    void approx_form() {
        assertThat(overwritable("C-0029 – ca. 1920", null)).isTrue();
    }

    @Test
    void month_form() {
        assertThat(overwritable("C-0029 – Juni 1916", null)).isTrue();
    }

    @Test
    void day_form() {
        assertThat(overwritable("C-0029 – 24. Dezember 1943", null)).isTrue();
    }

    @Test
    void season_form() {
        assertThat(overwritable("C-0029 – Sommer 1916", null)).isTrue();
    }

    @Test
    void unknown_label_form() {
        assertThat(overwritable("C-0029 – Datum unbekannt", null)).isTrue();
    }

    @Test
    void range_same_month_form() {
        assertThat(overwritable("C-0029 – 10.–11. Jan. 1917", null)).isTrue();
    }

    @Test
    void range_cross_month_form() {
        assertThat(overwritable("C-0029 – 30. Jan. – 2. Feb. 1917", null)).isTrue();
    }

    @Test
    void range_cross_year_form() {
        assertThat(overwritable("C-0029 – 30. Dez. 1916 – 2. Jan. 1917", null)).isTrue();
    }

    @Test
    void range_single_day_form() {
        assertThat(overwritable("C-0029 – 10. Jan. 1917", null)).isTrue();
    }

    @Test
    void range_open_form() {
        assertThat(overwritable("C-0029 – ab 10. Jan. 1917", null)).isTrue();
    }

    // ─── date label + trailing location (any location) ────────────────────────

    @Test
    void date_form_with_trailing_location() {
        assertThat(overwritable("C-0029 – 1916 – Berlin", null)).isTrue();
    }

    @Test
    void range_with_internal_separator_plus_trailing_location() {
        // The range label itself contains " – "; the trailing " – Berlin" must still be peeled.
        assertThat(overwritable("C-0029 – 30. Jan. – 2. Feb. 1917 – Berlin", null)).isTrue();
    }

    // ─── index-only and index+location cases ──────────────────────────────────

    @Test
    void exactly_index() {
        assertThat(overwritable("C-0029", null)).isTrue();
    }

    @Test
    void index_plus_location_equal_to_current() {
        assertThat(overwritable("C-0029 – Berlin", "Berlin")).isTrue();
    }

    // ─── prose is left untouched ──────────────────────────────────────────────

    @Test
    void prose_segment_not_matching_location_is_skipped() {
        assertThat(overwritable("C-0029 – Brief an Mutter", "Berlin")).isFalse();
    }

    @Test
    void location_only_segment_is_skipped_when_no_current_location() {
        // No date label, and the doc has no location to compare against → cannot prove machine.
        assertThat(overwritable("C-0029 – Berlin", null)).isFalse();
    }

    @Test
    void title_not_starting_with_index_is_skipped() {
        assertThat(overwritable("Ganz anderer Titel", null)).isFalse();
    }

    // ─── near-miss: shapes that look almost machine-built but are not ──────────

    @Test
    void ascii_hyphen_instead_of_en_dash_separator_is_skipped() {
        // The separator is " – " (en dash); a plain " - " is not the machine separator.
        assertThat(overwritable("C-0029 - 1916", null)).isFalse();
    }

    @Test
    void date_label_without_separator_before_trailing_text_is_skipped() {
        // "1916 Berlin" is not a date label and is not joined by " – "; prose, not machine.
        assertThat(overwritable("C-0029 – 1916 Berlin", null)).isFalse();
    }

    @Test
    void year_with_trailing_letters_is_not_a_year_label() {
        assertThat(overwritable("C-0029 – 1916er Brief", null)).isFalse();
    }

    @Test
    void index_immediately_followed_by_text_without_separator_is_skipped() {
        assertThat(overwritable("C-0029x – 1916", null)).isFalse();
    }

    // ─── fail-closed guards ───────────────────────────────────────────────────

    @Test
    void null_title_is_not_overwritable() {
        assertThat(DocumentTitleBackfillMatcher.isOverwritable(null, "C-0029", null)).isFalse();
    }

    @Test
    void null_index_is_not_overwritable() {
        assertThat(DocumentTitleBackfillMatcher.isOverwritable("C-0029 – 1916", null, null)).isFalse();
    }

    @Test
    void blank_index_is_not_overwritable() {
        assertThat(DocumentTitleBackfillMatcher.isOverwritable("  – 1916", "   ", null)).isFalse();
    }

    // ─── ReDoS / regex-metacharacter index is matched literally and terminates ─

    @Test
    @Timeout(value = 5, unit = TimeUnit.SECONDS)
    void index_with_regex_metacharacters_is_matched_literally_and_terminates() {
        String hostileIndex = "C-0029(.*).pdf";
        // Literal prefix → matches; trailing date label → overwritable. Must not hang.
        assertThat(DocumentTitleBackfillMatcher.isOverwritable(
                hostileIndex + " – 1916", hostileIndex, null)).isTrue();
        // A title that does NOT start with the literal hostile index is skipped, also fast.
        assertThat(DocumentTitleBackfillMatcher.isOverwritable(
                "C-0029 – 1916", hostileIndex, null)).isFalse();
    }
}
