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

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

import org.junit.jupiter.api.Test;

import de.gecheckt.pdf.umbenenner.domain.model.AiRawResponse;
import de.gecheckt.pdf.umbenenner.domain.model.AiResponseParsingFailure;
import de.gecheckt.pdf.umbenenner.domain.model.AiResponseParsingResult;
import de.gecheckt.pdf.umbenenner.domain.model.AiResponseParsingSuccess;
import de.gecheckt.pdf.umbenenner.domain.model.ParsedAiResponse;

/**
 * Unit tests for {@link AiResponseParser}.
 * <p>
 * Covers structural parsing rules: valid JSON objects, mandatory fields,
 * optional date, extra fields, extraction of JSON objects embedded in surrounding
 * text or Markdown code fences, and rejection of non-JSON or non-object responses.
 */
class AiResponseParserTest {

    // -------------------------------------------------------------------------
    // Success cases
    // -------------------------------------------------------------------------

    @Test
    void parse_validJsonWithAllFields_returnsSuccess() {
        AiRawResponse raw = new AiRawResponse(
                "{\"title\":\"Stromabrechnung\",\"reasoning\":\"Found bill dated 2026-01-15\",\"date\":\"2026-01-15\"}");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

        assertThat(result).isInstanceOf(AiResponseParsingSuccess.class);
        ParsedAiResponse parsed = ((AiResponseParsingSuccess) result).response();
        assertThat(parsed.title()).isEqualTo("Stromabrechnung");
        assertThat(parsed.reasoning()).isEqualTo("Found bill dated 2026-01-15");
        assertThat(parsed.dateString()).contains("2026-01-15");
    }

    @Test
    void parse_validJsonWithoutDate_returnsSuccessWithEmptyOptional() {
        AiRawResponse raw = new AiRawResponse(
                "{\"title\":\"Kontoauszug\",\"reasoning\":\"No date found in document\"}");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

        assertThat(result).isInstanceOf(AiResponseParsingSuccess.class);
        ParsedAiResponse parsed = ((AiResponseParsingSuccess) result).response();
        assertThat(parsed.title()).isEqualTo("Kontoauszug");
        assertThat(parsed.dateString()).isEmpty();
    }

    @Test
    void parse_validJsonWithAdditionalFields_toleratesExtraFields() {
        AiRawResponse raw = new AiRawResponse(
                "{\"title\":\"Rechnung\",\"reasoning\":\"Invoice\",\"confidence\":0.95,\"lang\":\"de\"}");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

        assertThat(result).isInstanceOf(AiResponseParsingSuccess.class);
        ParsedAiResponse parsed = ((AiResponseParsingSuccess) result).response();
        assertThat(parsed.title()).isEqualTo("Rechnung");
    }

    @Test
    void parse_validJsonWithLeadingAndTrailingWhitespace_trimsAndSucceeds() {
        AiRawResponse raw = new AiRawResponse(
                "   {\"title\":\"Vertrag\",\"reasoning\":\"Contract document\"}   ");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

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

    @Test
    void parse_emptyReasoningField_isAccepted() {
        AiRawResponse raw = new AiRawResponse(
                "{\"title\":\"Mahnung\",\"reasoning\":\"\"}");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

        assertThat(result).isInstanceOf(AiResponseParsingSuccess.class);
        ParsedAiResponse parsed = ((AiResponseParsingSuccess) result).response();
        assertThat(parsed.reasoning()).isEmpty();
    }

    @Test
    void parse_nullDateField_treatedAsAbsent() {
        AiRawResponse raw = new AiRawResponse(
                "{\"title\":\"Bescheid\",\"reasoning\":\"Administrative notice\",\"date\":null}");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

        assertThat(result).isInstanceOf(AiResponseParsingSuccess.class);
        ParsedAiResponse parsed = ((AiResponseParsingSuccess) result).response();
        assertThat(parsed.dateString()).isEmpty();
    }

    // -------------------------------------------------------------------------
    // Tolerance: JSON object embedded in surrounding text / Markdown fences
    // -------------------------------------------------------------------------

    @Test
    void parse_jsonWrappedInMarkdownFence_extractsObject() {
        AiRawResponse raw = new AiRawResponse(
                "```json\n{\"title\":\"Rechnung\",\"reasoning\":\"Invoice\"}\n```");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

        assertThat(result).isInstanceOf(AiResponseParsingSuccess.class);
        ParsedAiResponse parsed = ((AiResponseParsingSuccess) result).response();
        assertThat(parsed.title()).isEqualTo("Rechnung");
        assertThat(parsed.reasoning()).isEqualTo("Invoice");
    }

    @Test
    void parse_jsonWithLeadingProse_extractsObjectIncludingDate() {
        AiRawResponse raw = new AiRawResponse(
                "Hier ist das Ergebnis:\n{\"title\":\"Stromabrechnung\",\"reasoning\":\"r\",\"date\":\"2026-01-15\"}");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

        assertThat(result).isInstanceOf(AiResponseParsingSuccess.class);
        ParsedAiResponse parsed = ((AiResponseParsingSuccess) result).response();
        assertThat(parsed.title()).isEqualTo("Stromabrechnung");
        assertThat(parsed.dateString()).contains("2026-01-15");
    }

    @Test
    void parse_jsonWithTrailingProse_extractsObject() {
        AiRawResponse raw = new AiRawResponse(
                "{\"title\":\"Vertrag\",\"reasoning\":\"r\"}\n\nHope that helps!");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

        assertThat(result).isInstanceOf(AiResponseParsingSuccess.class);
        assertThat(((AiResponseParsingSuccess) result).response().title()).isEqualTo("Vertrag");
    }

    @Test
    void parse_bracketInsideStringValue_isNotTreatedAsArray() {
        AiRawResponse raw = new AiRawResponse(
                "{\"title\":\"Rechnung [Entwurf]\",\"reasoning\":\"contains a [ bracket\"}");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

        assertThat(result).isInstanceOf(AiResponseParsingSuccess.class);
        assertThat(((AiResponseParsingSuccess) result).response().title()).isEqualTo("Rechnung [Entwurf]");
    }

    @Test
    void parse_nestedObject_bracesAreBalancedCorrectly() {
        AiRawResponse raw = new AiRawResponse(
                "prefix {\"title\":\"X\",\"reasoning\":\"r\",\"meta\":{\"a\":1,\"b\":{\"c\":2}}} suffix");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

        assertThat(result).isInstanceOf(AiResponseParsingSuccess.class);
        assertThat(((AiResponseParsingSuccess) result).response().title()).isEqualTo("X");
    }

    @Test
    void parse_unbalancedTruncatedObject_returnsFailure() {
        AiRawResponse raw = new AiRawResponse(
                "{\"title\":\"X\",\"reasoning\":\"r\"");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

        assertThat(result).isInstanceOf(AiResponseParsingFailure.class);
        assertThat(((AiResponseParsingFailure) result).failureReason())
                .isEqualTo("NOT_JSON_OBJECT");
    }

    // -------------------------------------------------------------------------
    // Failure cases – structural
    // -------------------------------------------------------------------------

    @Test
    void parse_emptyBody_returnsFailure() {
        AiRawResponse raw = new AiRawResponse("");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

        assertThat(result).isInstanceOf(AiResponseParsingFailure.class);
        assertThat(((AiResponseParsingFailure) result).failureReason())
                .isEqualTo("EMPTY_RESPONSE");
    }

    @Test
    void parse_blankBody_returnsFailure() {
        AiRawResponse raw = new AiRawResponse("   \t\n  ");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

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

    @Test
    void parse_plainText_returnsFailure() {
        AiRawResponse raw = new AiRawResponse("Sure, here is the title: Rechnung");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

        assertThat(result).isInstanceOf(AiResponseParsingFailure.class);
        assertThat(((AiResponseParsingFailure) result).failureReason())
                .isEqualTo("NOT_JSON_OBJECT");
    }

    @Test
    void parse_jsonArray_returnsFailure() {
        AiRawResponse raw = new AiRawResponse("[{\"title\":\"Rechnung\",\"reasoning\":\"r\"}]");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

        assertThat(result).isInstanceOf(AiResponseParsingFailure.class);
        assertThat(((AiResponseParsingFailure) result).failureReason())
                .isEqualTo("NOT_JSON_OBJECT");
    }

    @Test
    void parse_invalidJson_returnsFailure() {
        AiRawResponse raw = new AiRawResponse("{\"title\":\"Rechnung\",\"reasoning\":}");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

        assertThat(result).isInstanceOf(AiResponseParsingFailure.class);
        assertThat(((AiResponseParsingFailure) result).failureReason())
                .isEqualTo("INVALID_JSON");
    }

    @Test
    void parse_missingTitle_returnsFailure() {
        AiRawResponse raw = new AiRawResponse("{\"reasoning\":\"Some reasoning without title\"}");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

        assertThat(result).isInstanceOf(AiResponseParsingFailure.class);
        assertThat(((AiResponseParsingFailure) result).failureReason())
                .isEqualTo("MISSING_TITLE");
    }

    @Test
    void parse_nullTitle_returnsFailure() {
        AiRawResponse raw = new AiRawResponse("{\"title\":null,\"reasoning\":\"r\"}");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

        assertThat(result).isInstanceOf(AiResponseParsingFailure.class);
        assertThat(((AiResponseParsingFailure) result).failureReason())
                .isEqualTo("MISSING_TITLE");
    }

    @Test
    void parse_blankTitle_returnsFailure() {
        AiRawResponse raw = new AiRawResponse("{\"title\":\"   \",\"reasoning\":\"r\"}");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

        assertThat(result).isInstanceOf(AiResponseParsingFailure.class);
        assertThat(((AiResponseParsingFailure) result).failureReason())
                .isEqualTo("BLANK_TITLE");
    }

    @Test
    void parse_missingReasoning_returnsFailure() {
        AiRawResponse raw = new AiRawResponse("{\"title\":\"Rechnung\"}");

        AiResponseParsingResult result = AiResponseParser.parse(raw);

        assertThat(result).isInstanceOf(AiResponseParsingFailure.class);
        assertThat(((AiResponseParsingFailure) result).failureReason())
                .isEqualTo("MISSING_REASONING");
    }

    @Test
    void parse_nullRawResponse_throwsNullPointerException() {
        assertThatThrownBy(() -> AiResponseParser.parse(null))
                .isInstanceOf(NullPointerException.class)
                .hasMessage("rawResponse must not be null");
    }
}
