package de.gecheckt.pdf.umbenenner.bootstrap.adapter;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;

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

import de.gecheckt.pdf.umbenenner.application.port.out.AiContentSensitivity;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;

/**
 * Unit tests for {@link Log4jProcessingLogger}.
 * <p>
 * Tests the Log4j2 adapter's compliance with the {@link ProcessingLogger} port interface,
 * including correct delegation to Log4j2 and special handling of Throwable arguments
 * in the error method.
 */
class Log4jProcessingLoggerTest {

    private Log4jProcessingLogger logger;
    private TestLogCapture logCapture;

    @BeforeEach
    void setUp() {
        // Create logger for this test class
        logger = new Log4jProcessingLogger(Log4jProcessingLoggerTest.class);
    }

    @AfterEach
    void tearDownCapture() {
        if (logCapture != null) {
            logCapture.stopCapture();
            logCapture = null;
        }
    }

    @Test
    void constructor_createsLoggerForClass() {
        // Verify that a logger is successfully created for the given class
        assertNotNull(logger, "Logger should be created successfully");

        // Create logger with different class
        Log4jProcessingLogger otherLogger = new Log4jProcessingLogger(String.class);
        assertNotNull(otherLogger, "Logger should be creatable for different classes");
    }

    @Test
    void info_withoutArgs_logsMessage() {
        // Verify info method accepts message without arguments
        assertDoesNotThrow(() -> {
            logger.info("Test info message");
        }, "info() should accept message without arguments");
    }

    @Test
    void info_withArgs_logsMessageWithSubstitution() {
        // Verify info method accepts message with format arguments
        assertDoesNotThrow(() -> {
            logger.info("Test info with arg: {}", "argument-value");
        }, "info() should accept message with format arguments");
    }

    @Test
    void info_withMultipleArgs_logsMessageWithAllArgs() {
        // Verify info method handles multiple arguments correctly
        assertDoesNotThrow(() -> {
            logger.info("Test with multiple args: {} and {}", "arg1", "arg2");
        }, "info() should handle multiple format arguments");
    }

    @Test
    void debug_withoutArgs_logsMessage() {
        // Verify debug method accepts message without arguments
        assertDoesNotThrow(() -> {
            logger.debug("Test debug message");
        }, "debug() should accept message without arguments");
    }

    @Test
    void debug_withArgs_logsMessageWithSubstitution() {
        // Verify debug method accepts message with format arguments
        assertDoesNotThrow(() -> {
            logger.debug("Test debug with arg: {}", "argument-value");
        }, "debug() should accept message with format arguments");
    }

    @Test
    void warn_withoutArgs_logsMessage() {
        // Verify warn method accepts message without arguments
        assertDoesNotThrow(() -> {
            logger.warn("Test warn message");
        }, "warn() should accept message without arguments");
    }

    @Test
    void warn_withArgs_logsMessageWithSubstitution() {
        // Verify warn method accepts message with format arguments
        assertDoesNotThrow(() -> {
            logger.warn("Test warn with arg: {}", "argument-value");
        }, "warn() should accept message with format arguments");
    }

    @Test
    void error_withoutArgs_logsMessage() {
        // Verify error method accepts message without arguments
        assertDoesNotThrow(() -> {
            logger.error("Test error message");
        }, "error() should accept message without arguments");
    }

    @Test
    void error_withFormatArgs_logsMessageWithSubstitution() {
        // Verify error method accepts message with format arguments
        assertDoesNotThrow(() -> {
            logger.error("Test error with arg: {}", "argument-value");
        }, "error() should accept message with format arguments");
    }

    @Test
    void error_withMultipleFormatArgs_logsMessageWithAllArgs() {
        // Verify error method handles multiple format arguments correctly
        assertDoesNotThrow(() -> {
            logger.error("Test error with multiple args: {} and {}", "arg1", "arg2");
        }, "error() should handle multiple format arguments");
    }

    @Test
    void error_withThrowableAsLastArg_extractsAndLogsThrowable() {
        // Verify that when the last argument is a Throwable,
        // it is extracted and passed separately to Log4j2, not as a format argument.
        Exception testException = new IllegalStateException("Test exception message");

        assertDoesNotThrow(() -> {
            logger.error("Error occurred: {}", testException);
        }, "error() should handle Throwable as last argument without throwing");
    }

    @Test
    void error_withMessageArgsAndThrowable_separatesThrowableCorrectly() {
        // Verify error() correctly separates message format args from the Throwable
        // when the Throwable is the last argument
        Exception testException = new RuntimeException("Test runtime exception");

        assertDoesNotThrow(() -> {
            logger.error("Operation failed for document: {} with error", "doc-123", testException);
        }, "error() should handle format args followed by Throwable");
    }

    @Test
    void error_withOnlyThrowableAsArg_logsThrowableCorrectly() {
        // Verify error() works when only a Throwable is passed as argument
        Exception testException = new IOException("I/O error");

        assertDoesNotThrow(() -> {
            logger.error("An error occurred", testException);
        }, "error() should handle message with only Throwable");
    }

    @Test
    void error_withThrowableNotAsLastArg_treatsAsFormatArg() {
        // Verify that if a Throwable is NOT the last argument, it is treated as a format arg
        // (This is an edge case: the last arg must be Throwable for special handling)
        Exception testException = new Exception("Test");

        assertDoesNotThrow(() -> {
            logger.error("Error: {} with info {}", testException, "additional-data");
        }, "error() should handle Throwable as non-last argument as format parameter");
    }

    /**
     * Verifies that format arguments before the trailing Throwable are preserved in the
     * formatted log message. Without a correct arraycopy, the message args would be empty
     * (all nulls) and the substituted values would not appear in the log output.
     */
    @Test
    void error_withFormatArgsAndThrowable_preservesFormatArgsInMessage() {
        String expectedArg = "EXPECTED_FORMAT_ARG_7c4a3b1e";
        logCapture = new TestLogCapture();
        logCapture.startCapture();
        logger.error("Processing failed for {}", expectedArg, new RuntimeException("test failure"));
        logCapture.stopCapture();

        assertTrue(logCapture.containsMessage(expectedArg),
                "Format args before the trailing Throwable must appear in the formatted log message");
    }

    @Test
    void implementsProcessingLoggerInterface() {
        // Verify that Log4jProcessingLogger correctly implements ProcessingLogger
        assertTrue(logger instanceof ProcessingLogger,
                "Log4jProcessingLogger should implement ProcessingLogger interface");
    }

    @Test
    void allLogLevels_executeWithoutException() {
        // Comprehensive test verifying all log methods work without throwing
        String testMessage = "Test message";
        Object arg1 = "argument";
        Exception exception = new Exception("test");

        assertAll("All log methods should execute without exception",
            () -> assertDoesNotThrow(() -> logger.info(testMessage)),
            () -> assertDoesNotThrow(() -> logger.info(testMessage, arg1)),
            () -> assertDoesNotThrow(() -> logger.debug(testMessage)),
            () -> assertDoesNotThrow(() -> logger.debug(testMessage, arg1)),
            () -> assertDoesNotThrow(() -> logger.warn(testMessage)),
            () -> assertDoesNotThrow(() -> logger.warn(testMessage, arg1)),
            () -> assertDoesNotThrow(() -> logger.error(testMessage)),
            () -> assertDoesNotThrow(() -> logger.error(testMessage, arg1)),
            () -> assertDoesNotThrow(() -> logger.error(testMessage, exception))
        );
    }

    @Test
    void debugSensitiveAiContent_withDefaultSensitivity_acceptsMessage() {
        // Verify debugSensitiveAiContent method exists and executes with default PROTECT sensitivity
        Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
                Log4jProcessingLoggerTest.class,
                AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);

        assertDoesNotThrow(() -> {
            protectedLogger.debugSensitiveAiContent("Sensitive content: {}", "raw AI response");
        }, "debugSensitiveAiContent() should execute without throwing with PROTECT_SENSITIVE_CONTENT");
    }

    @Test
    void debugSensitiveAiContent_withLogSensitivity_acceptsMessage() {
        // Verify debugSensitiveAiContent method executes with LOG_SENSITIVE_CONTENT setting
        Log4jProcessingLogger logSensitiveLogger = new Log4jProcessingLogger(
                Log4jProcessingLoggerTest.class,
                AiContentSensitivity.LOG_SENSITIVE_CONTENT);

        assertDoesNotThrow(() -> {
            logSensitiveLogger.debugSensitiveAiContent("AI reasoning: {}", "complete reasoning text");
        }, "debugSensitiveAiContent() should execute without throwing with LOG_SENSITIVE_CONTENT");
    }

    @Test
    void constructorWithSensitivity_acceptsProtectSensitiveContent() {
        // Verify constructor accepts PROTECT_SENSITIVE_CONTENT
        Log4jProcessingLogger logger1 = new Log4jProcessingLogger(
                Log4jProcessingLoggerTest.class,
                AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
        assertNotNull(logger1, "Logger should be created with PROTECT_SENSITIVE_CONTENT");
    }

    @Test
    void constructorWithSensitivity_acceptsLogSensitiveContent() {
        // Verify constructor accepts LOG_SENSITIVE_CONTENT
        Log4jProcessingLogger logger2 = new Log4jProcessingLogger(
                Log4jProcessingLoggerTest.class,
                AiContentSensitivity.LOG_SENSITIVE_CONTENT);
        assertNotNull(logger2, "Logger should be created with LOG_SENSITIVE_CONTENT");
    }

    @Test
    void constructorWithSensitivity_rejectNullSensitivity() {
        // Verify constructor requires non-null AiContentSensitivity
        assertThrows(NullPointerException.class, () -> {
            new Log4jProcessingLogger(Log4jProcessingLoggerTest.class, null);
        }, "Constructor should reject null AiContentSensitivity");
    }

    // ===== Behavioral tests: real log capture verification =====

    /**
     * Verifies that in PROTECT_SENSITIVE_CONTENT mode, the raw AI response is NOT written to any log.
     * Uses {@link TestLogCapture} to prove suppression with real log output.
     */
    @Test
    void debugSensitiveAiContent_protectionMode_rawResponseNotLogged() {
        Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
                Log4jProcessingLoggerTest.class,
                AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
        String sensitiveContent = "PROTECT_RAW_RESPONSE_9f3e2b1a";

        logCapture = new TestLogCapture();
        logCapture.startCapture();
        protectedLogger.debugSensitiveAiContent("AI response: {}", sensitiveContent);
        logCapture.stopCapture();

        assertFalse(logCapture.containsMessage(sensitiveContent),
                "PROTECT_SENSITIVE_CONTENT mode must not write raw AI response to log");
    }

    /**
     * Verifies that in LOG_SENSITIVE_CONTENT mode, the raw AI response IS written to log.
     * Uses {@link TestLogCapture} to prove emission with real log output.
     */
    @Test
    void debugSensitiveAiContent_optInMode_rawResponseLogged() {
        Log4jProcessingLogger optInLogger = new Log4jProcessingLogger(
                Log4jProcessingLoggerTest.class,
                AiContentSensitivity.LOG_SENSITIVE_CONTENT);
        String sensitiveContent = "OPTIN_RAW_RESPONSE_7d4c8f2e";

        logCapture = new TestLogCapture();
        logCapture.startCapture();
        optInLogger.debugSensitiveAiContent("AI response: {}", sensitiveContent);
        logCapture.stopCapture();

        assertTrue(logCapture.containsMessage(sensitiveContent),
                "LOG_SENSITIVE_CONTENT mode must write raw AI response to log when explicitly enabled");
    }

    /**
     * Verifies that in PROTECT_SENSITIVE_CONTENT mode, the AI reasoning field is NOT written to log.
     * Uses {@link TestLogCapture} to prove suppression of the reasoning field specifically.
     */
    @Test
    void debugSensitiveAiContent_protectionMode_reasoningNotLogged() {
        Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
                Log4jProcessingLoggerTest.class,
                AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
        String sensitiveReasoning = "PROTECT_REASONING_c8a1e5f3";

        logCapture = new TestLogCapture();
        logCapture.startCapture();
        protectedLogger.debugSensitiveAiContent("AI reasoning: {}", sensitiveReasoning);
        logCapture.stopCapture();

        assertFalse(logCapture.containsMessage(sensitiveReasoning),
                "PROTECT_SENSITIVE_CONTENT mode must not write AI reasoning to log");
    }

    /**
     * Verifies that in LOG_SENSITIVE_CONTENT mode, the AI reasoning field IS written to log.
     * Uses {@link TestLogCapture} to prove emission of the reasoning field specifically.
     */
    @Test
    void debugSensitiveAiContent_optInMode_reasoningLogged() {
        Log4jProcessingLogger optInLogger = new Log4jProcessingLogger(
                Log4jProcessingLoggerTest.class,
                AiContentSensitivity.LOG_SENSITIVE_CONTENT);
        String sensitiveReasoning = "OPTIN_REASONING_b2d6f9a4";

        logCapture = new TestLogCapture();
        logCapture.startCapture();
        optInLogger.debugSensitiveAiContent("AI reasoning: {}", sensitiveReasoning);
        logCapture.stopCapture();

        assertTrue(logCapture.containsMessage(sensitiveReasoning),
                "LOG_SENSITIVE_CONTENT mode must write AI reasoning to log when explicitly enabled");
    }

    /**
     * Verifies the behavioral contrast between protection and opt-in mode using real log capture.
     * <p>
     * The same sensitive content token is logged in both modes: protection mode must suppress it,
     * opt-in mode must emit it. Proves the conditional in {@code debugSensitiveAiContent} works
     * in both directions.
     */
    @Test
    void debugSensitiveAiContent_modeContrast_differentLogOutput() {
        String sensitiveContent = "CONTRAST_SENSITIVE_d3e7a1b8";

        Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
                Log4jProcessingLoggerTest.class,
                AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
        Log4jProcessingLogger optInLogger = new Log4jProcessingLogger(
                Log4jProcessingLoggerTest.class,
                AiContentSensitivity.LOG_SENSITIVE_CONTENT);

        // Protection mode: content must NOT appear in logs
        logCapture = new TestLogCapture();
        logCapture.startCapture();
        protectedLogger.debugSensitiveAiContent("AI: {}", sensitiveContent);
        logCapture.stopCapture();
        assertFalse(logCapture.containsMessage(sensitiveContent),
                "PROTECT mode must suppress sensitive content");

        // Opt-in mode: same content MUST appear in logs
        logCapture = new TestLogCapture();
        logCapture.startCapture();
        optInLogger.debugSensitiveAiContent("AI: {}", sensitiveContent);
        logCapture.stopCapture();
        assertTrue(logCapture.containsMessage(sensitiveContent),
                "LOG mode must emit sensitive content");
    }

    /**
     * Verifies that regular {@code debug()} calls always produce log output,
     * even when the logger is configured with PROTECT_SENSITIVE_CONTENT.
     * <p>
     * The sensitivity rule applies only to {@code debugSensitiveAiContent()},
     * not to the regular logging methods.
     */
    @Test
    void regularDebugMethod_alwaysLogged_unaffectedBySensitivity() {
        Log4jProcessingLogger protectedLogger = new Log4jProcessingLogger(
                Log4jProcessingLoggerTest.class,
                AiContentSensitivity.PROTECT_SENSITIVE_CONTENT);
        String regularMarker = "REGULAR_DEBUG_e6f0c2d9";

        logCapture = new TestLogCapture();
        logCapture.startCapture();
        protectedLogger.debug("Regular: {}", regularMarker);
        logCapture.stopCapture();

        assertTrue(logCapture.containsMessage(regularMarker),
                "Regular debug() must always produce log output regardless of sensitivity setting");
    }

}
