package de.gecheckt.pdf.umbenenner.adapter.out.ai;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.List;
import java.util.UUID;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.junit.jupiter.MockitoExtension;

import de.gecheckt.pdf.umbenenner.adapter.out.clock.SystemClockAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.fingerprint.Sha256FingerprintAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.lock.FilesystemRunLockPortAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.pdfextraction.PdfTextExtractionPortAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.prompt.FilesystemPromptPortAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.sourcedocument.SourceDocumentCandidatesPortAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteDocumentRecordRepositoryAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteProcessingAttemptRepositoryAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteSchemaInitializationAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteUnitOfWorkAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.targetcopy.FilesystemTargetFileCopyAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.targetfolder.FilesystemTargetFolderAdapter;
import de.gecheckt.pdf.umbenenner.application.config.RuntimeConfiguration;
import de.gecheckt.pdf.umbenenner.application.config.provider.ProviderConfiguration;
import de.gecheckt.pdf.umbenenner.application.port.out.AiContentSensitivity;
import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintSuccess;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;
import de.gecheckt.pdf.umbenenner.application.service.AiNamingService;
import de.gecheckt.pdf.umbenenner.application.service.AiResponseValidator;
import de.gecheckt.pdf.umbenenner.application.service.DocumentProcessingCoordinator;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultBatchRunProcessingUseCase;
import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
import de.gecheckt.pdf.umbenenner.domain.model.RunId;
import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate;
import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentLocator;

/**
 * Integration test verifying that the Anthropic Claude adapter integrates correctly
 * with the full batch processing pipeline and that the provider identifier
 * {@code "claude"} is persisted in the processing attempt history.
 * <p>
 * Uses a mocked HTTP client to simulate the Anthropic API without real network calls.
 * All other adapters (SQLite, filesystem, PDF extraction, fingerprinting) are real
 * production implementations.
 */
@ExtendWith(MockitoExtension.class)
@DisplayName("AnthropicClaudeAdapter integration")
class AnthropicClaudeAdapterIntegrationTest {

    /**
     * Pflicht-Testfall 15: claudeProviderIdentifierLandsInAttemptHistory
     * <p>
     * Verifies the end-to-end integration: the Claude adapter with a mocked HTTP layer
     * is wired into the batch pipeline, and after a successful run, the processing attempt
     * record contains {@code ai_provider='claude'}.
     */
    @Test
    @DisplayName("claudeProviderIdentifierLandsInAttemptHistory: ai_provider=claude in attempt history after successful run")
    void claudeProviderIdentifierLandsInAttemptHistory(@TempDir Path tempDir) throws Exception {
        // --- Infrastructure setup ---
        Path sourceFolder = Files.createDirectories(tempDir.resolve("source"));
        Path targetFolder = Files.createDirectories(tempDir.resolve("target"));
        Path promptFile = tempDir.resolve("prompt.txt");
        Files.writeString(promptFile, "Analysiere das Dokument und liefere JSON.");

        String jdbcUrl = "jdbc:sqlite:" + tempDir.resolve("test.db")
                .toAbsolutePath().toString().replace('\\', '/');
        new SqliteSchemaInitializationAdapter(jdbcUrl).initializeSchema();

        // --- Create a searchable PDF in the source folder ---
        Path pdfPath = sourceFolder.resolve("testdokument.pdf");
        createSearchablePdf(pdfPath, "Testinhalt Rechnung Datum 15.01.2024 Betrag 99 EUR");

        // --- Compute fingerprint for later verification ---
        Sha256FingerprintAdapter fingerprintAdapter = new Sha256FingerprintAdapter();
        SourceDocumentCandidate candidate = new SourceDocumentCandidate(
                pdfPath.getFileName().toString(), 0L,
                new SourceDocumentLocator(pdfPath.toAbsolutePath().toString()));
        DocumentFingerprint fingerprint = switch (fingerprintAdapter.computeFingerprint(candidate)) {
            case FingerprintSuccess s -> s.fingerprint();
            default -> throw new IllegalStateException("Fingerprint computation failed");
        };

        // --- Mock the HTTP client for the Claude adapter ---
        HttpClient mockHttpClient = mock(HttpClient.class);
        // Build a valid Anthropic response with the NamingProposal JSON as text content
        String namingProposalJson =
                "{\\\"date\\\":\\\"2024-01-15\\\",\\\"title\\\":\\\"Testrechnung\\\","
                + "\\\"reasoning\\\":\\\"Rechnung vom 15.01.2024\\\"}";
        String anthropicResponseBody = "{"
                + "\"id\":\"msg_integration_test\","
                + "\"type\":\"message\","
                + "\"role\":\"assistant\","
                + "\"content\":[{\"type\":\"text\",\"text\":\"" + namingProposalJson + "\"}],"
                + "\"stop_reason\":\"end_turn\""
                + "}";

        HttpResponse<String> mockHttpResponse = mockStringResponse(200, anthropicResponseBody);
        doReturn(mockHttpResponse).when(mockHttpClient).send(any(HttpRequest.class), any());

        // --- Create the Claude adapter with the mocked HTTP client ---
        ProviderConfiguration claudeConfig = new ProviderConfiguration(
                "claude-3-5-sonnet-20241022", 60, "https://api.anthropic.com", "sk-ant-test");
        AnthropicClaudeHttpAdapter claudeAdapter =
                new AnthropicClaudeHttpAdapter(claudeConfig, mockHttpClient);

        // --- Wire the full pipeline with provider identifier "claude" ---
        SqliteDocumentRecordRepositoryAdapter documentRepo =
                new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);
        SqliteProcessingAttemptRepositoryAdapter attemptRepo =
                new SqliteProcessingAttemptRepositoryAdapter(jdbcUrl);
        SqliteUnitOfWorkAdapter unitOfWork = new SqliteUnitOfWorkAdapter(jdbcUrl);

        ProcessingLogger noOpLogger = new NoOpProcessingLogger();
        DocumentProcessingCoordinator coordinator = new DocumentProcessingCoordinator(
                documentRepo, attemptRepo, unitOfWork,
                new FilesystemTargetFolderAdapter(targetFolder),
                new FilesystemTargetFileCopyAdapter(targetFolder),
                noOpLogger,
                3,
                60,
                "claude");  // provider identifier for Claude

        AiNamingService aiNamingService = new AiNamingService(
                claudeAdapter,
                new FilesystemPromptPortAdapter(promptFile),
                new AiResponseValidator(new SystemClockAdapter(), 60),
                "claude-3-5-sonnet-20241022",
                10_000,
                60);

        DefaultBatchRunProcessingUseCase useCase = new DefaultBatchRunProcessingUseCase(
                new RuntimeConfiguration(50, 3, AiContentSensitivity.PROTECT_SENSITIVE_CONTENT),
                new FilesystemRunLockPortAdapter(tempDir.resolve("run.lock")),
                new SourceDocumentCandidatesPortAdapter(sourceFolder),
                new PdfTextExtractionPortAdapter(),
                fingerprintAdapter,
                coordinator,
                aiNamingService,
                noOpLogger);

        // --- Run the batch ---
        BatchRunContext context = new BatchRunContext(
                new RunId(UUID.randomUUID().toString()), Instant.now());
        useCase.execute(context);

        // --- Verify: ai_provider='claude' is stored in the attempt history ---
        List<ProcessingAttempt> attempts = attemptRepo.findAllByFingerprint(fingerprint);
        assertThat(attempts)
                .as("At least one attempt must be recorded")
                .isNotEmpty();
        assertThat(attempts.get(0).aiProvider())
                .as("Provider identifier must be 'claude' in the attempt history")
                .isEqualTo("claude");
    }

    // =========================================================================
    // Helpers
    // =========================================================================

    /**
     * Creates a typed mock {@link HttpResponse} to avoid unchecked-cast warnings at call sites.
     * The suppression is confined to this helper because the raw-type cast is technically
     * unavoidable due to type erasure when mocking generic interfaces.
     */
    @SuppressWarnings("unchecked")
    private static HttpResponse<String> mockStringResponse(int statusCode, String body) {
        HttpResponse<String> response = (HttpResponse<String>) mock(HttpResponse.class);
        when(response.statusCode()).thenReturn(statusCode);
        when(response.body()).thenReturn(body);
        return response;
    }

    /**
     * Creates a single-page searchable PDF with embedded text using PDFBox.
     */
    private static void createSearchablePdf(Path pdfPath, String text) throws Exception {
        try (PDDocument doc = new PDDocument()) {
            PDPage page = new PDPage();
            doc.addPage(page);
            try (PDPageContentStream cs = new PDPageContentStream(doc, page)) {
                cs.beginText();
                cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
                cs.newLineAtOffset(50, 700);
                cs.showText(text);
                cs.endText();
            }
            doc.save(pdfPath.toFile());
        }
    }

    /**
     * No-op implementation of {@link ProcessingLogger} for use in integration tests
     * where log output is not relevant to the assertion.
     */
    private static class NoOpProcessingLogger implements ProcessingLogger {
        @Override public void info(String message, Object... args) {
            // intentionally empty
        }
        @Override public void debug(String message, Object... args) {
            // intentionally empty
        }
        @Override public void warn(String message, Object... args) {
            // intentionally empty
        }
        @Override public void error(String message, Object... args) {
            // intentionally empty
        }
        @Override public void debugSensitiveAiContent(String message, Object... args) {
            // intentionally empty
        }
    }
}
