package de.gecheckt.pdf.umbenenner.application.validation.editor;

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

import java.util.List;

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

import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor;

/**
 * Unit-Tests für den {@link EditorConfigurationValidator}.
 * <p>
 * Prüft alle Validierungsregeln auf Basis synthetischer Eingaben ohne I/O.
 */
class EditorConfigurationValidatorTest {

    private EditorConfigurationValidator validator;

    @BeforeEach
    void setUp() {
        validator = new EditorConfigurationValidator();
    }

    // =========================================================================
    // Hilfsmethode: minimale gültige Eingabe
    // =========================================================================

    private static EditorValidationInput minimalValidInput() {
        return new EditorValidationInput(
                "claude",                    // activeProviderIdentifier
                "C:/source",                 // sourceFolder
                "C:/target",                 // targetFolder
                "C:/db.sqlite",              // sqliteFile
                "C:/prompt.txt",             // promptTemplateFile
                "3",                         // maxRetriesTransient
                "10",                        // maxPages
                "500",                       // maxTextCharacters
                "60",                        // maxTitleLength
                "https://api.anthropic.com", // claudeBaseUrl
                "claude-3-5-sonnet",         // claudeModel
                "30",                        // claudeTimeoutSeconds
                EffectiveApiKeyDescriptor.fromPropertyFile(), // claudeApiKeyDescriptor
                "sk-claude",                 // claudeApiKeyPropertyValue
                "https://api.openai.com",    // openaiBaseUrl
                "gpt-4",                     // openaiModel
                "30",                        // openaiTimeoutSeconds
                EffectiveApiKeyDescriptor.fromPropertyFile(), // openaiApiKeyDescriptor
                "sk-openai"                  // openaiApiKeyPropertyValue
        );
    }

    // =========================================================================
    // Leere Eingabe / Null-Checks
    // =========================================================================

    @Test
    void validate_emptyActiveProvider_producesError() {
        EditorValidationInput input = new EditorValidationInput(
                "", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "10", "500", "60",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.hasErrors()).isTrue();
        assertThat(report.findings()).anyMatch(f ->
                f.fieldKey().isPresent()
                && f.fieldKey().get().equals(EditorConfigurationValidator.FIELD_ACTIVE_PROVIDER)
                && f.severity() == EditorValidationSeverity.ERROR);
    }

    @Test
    void validate_unknownActiveProvider_producesError() {
        EditorValidationInput input = new EditorValidationInput(
                "unknown-provider", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "10", "500", "60",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.hasErrors()).isTrue();
        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_ACTIVE_PROVIDER))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
    }

    // =========================================================================
    // Pflichtpfade
    // =========================================================================

    @Test
    void validate_emptySourceFolder_producesError() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "10", "500", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_SOURCE_FOLDER))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
    }

    @Test
    void validate_emptyTargetFolder_producesError() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "", "C:/db.sqlite", "C:/prompt.txt",
                "3", "10", "500", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_TARGET_FOLDER))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
    }

    @Test
    void validate_emptySqliteFile_producesError() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "", "C:/prompt.txt",
                "3", "10", "500", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_SQLITE_FILE))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
    }

    @Test
    void validate_emptyPromptFile_producesError() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "",
                "3", "10", "500", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_PROMPT_FILE))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
    }

    // =========================================================================
    // max.retries.transient
    // =========================================================================

    @Test
    void validate_maxRetriesTransient_zero_producesError() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "0", "10", "500", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_RETRIES))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
    }

    @Test
    void validate_maxRetriesTransient_negative_producesError() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "-1", "10", "500", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_RETRIES))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
    }

    @Test
    void validate_maxRetriesTransient_one_producesNoError() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "1", "10", "500", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        List<EditorValidationFinding> retryFindings =
                report.findingsFor(EditorConfigurationValidator.FIELD_MAX_RETRIES);
        assertThat(retryFindings).noneMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
    }

    @Test
    void validate_maxRetriesTransient_nonNumeric_producesError() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "abc", "10", "500", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_RETRIES))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
    }

    // =========================================================================
    // max.pages
    // =========================================================================

    @Test
    void validate_maxPages_zero_producesError() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "0", "500", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_PAGES))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
    }

    @Test
    void validate_maxPages_over100_producesHint() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "101", "500", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_PAGES))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.HINT);
    }

    @Test
    void validate_maxPages_exactly100_producesNoHintAndNoError() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "100", "500", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        List<EditorValidationFinding> pageFindings =
                report.findingsFor(EditorConfigurationValidator.FIELD_MAX_PAGES);
        assertThat(pageFindings).noneMatch(f ->
                f.severity() == EditorValidationSeverity.ERROR
                || f.severity() == EditorValidationSeverity.HINT);
    }

    // =========================================================================
    // max.text.characters – Wirtschaftliche Warnlogik
    // =========================================================================

    @Test
    void validate_maxTextCharacters_1000_producesNoFinding() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "10", "1000", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_CHARS))
                .isEmpty();
    }

    @Test
    void validate_maxTextCharacters_1001_producesWarning() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "10", "1001", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_CHARS))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.WARNING);
    }

    @Test
    void validate_maxTextCharacters_3000_producesWarning() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "10", "3000", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_CHARS))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.WARNING);
    }

    @Test
    void validate_maxTextCharacters_3001_producesStrongWarning() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "10", "3001", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        List<EditorValidationFinding> charFindings =
                report.findingsFor(EditorConfigurationValidator.FIELD_MAX_CHARS);
        assertThat(charFindings).anyMatch(f -> f.severity() == EditorValidationSeverity.WARNING);
        // Starke Warnung enthält "stark" oder "riskant"
        assertThat(charFindings).anyMatch(f ->
                f.message().toLowerCase().contains("stark")
                || f.message().toLowerCase().contains("riskant"));
    }

    @Test
    void validate_maxTextCharacters_zero_producesError() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "10", "0", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_CHARS))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
    }

    // =========================================================================
    // Provider-Felder: Claude
    // =========================================================================

    @Test
    void validate_claude_emptyModel_producesError() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "10", "500", "60",
                "https://api.anthropic.com", "", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_CLAUDE_MODEL))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
    }

    @Test
    void validate_claude_emptyBaseUrl_producesWarning() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "10", "500", "60",
                "", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_CLAUDE_BASE_URL))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.WARNING);
    }

    @Test
    void validate_claude_negativeTimeout_producesError() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "10", "500", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "-5",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_CLAUDE_TIMEOUT))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
    }

    // =========================================================================
    // API-Key-Vorrangregel
    // =========================================================================

    @Test
    void validate_claude_absent_apiKey_producesWarning() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "10", "500", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.absent(), "",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_CLAUDE_API_KEY))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.WARNING);
    }

    @Test
    void validate_claude_fromPropertyFile_producesNoFinding() {
        EditorValidationInput input = minimalValidInput();
        // minimalValidInput() already uses fromPropertyFile() for Claude

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_CLAUDE_API_KEY))
                .isEmpty();
    }

    @Test
    void validate_claude_fromEnvVar_producesInfoFinding() {
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "10", "500", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromProviderEnvVar("ANTHROPIC_API_KEY"), "",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_CLAUDE_API_KEY))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.INFO
                        && f.message().contains("ANTHROPIC_API_KEY"));
    }

    @Test
    void validate_openai_fromLegacyEnvVar_producesInfoFinding() {
        EditorValidationInput input = new EditorValidationInput(
                "openai-compatible", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "10", "500", "60",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "",
                "https://api.openai.com", "gpt-4", "30",
                EffectiveApiKeyDescriptor.fromLegacyEnvVar("PDF_UMBENENNER_API_KEY"), "");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_OPENAI_API_KEY))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.INFO
                        && f.message().contains("PDF_UMBENENNER_API_KEY"));
    }

    // =========================================================================
    // Vollständig gültige Konfiguration
    // =========================================================================

    @Test
    void validate_fullyValidClaudeConfig_producesNoErrors() {
        EditorValidationInput input = minimalValidInput();

        EditorValidationReport report = validator.validate(input);

        assertThat(report.hasErrors()).isFalse();
    }

    @Test
    void validate_fullyValidOpenAiConfig_producesNoErrors() {
        EditorValidationInput input = new EditorValidationInput(
                "openai-compatible", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "10", "500", "60",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "",
                "https://api.openai.com", "gpt-4", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-openai");

        EditorValidationReport report = validator.validate(input);

        assertThat(report.hasErrors()).isFalse();
    }

    // =========================================================================
    // Inaktiver Provider – keine Fehler für inaktiven Block
    // =========================================================================

    @Test
    void validate_claude_active_openaiBlockEmpty_producesNoOpenaiErrors() {
        // Claude ist aktiv; OpenAI-Felder sind leer – darf keinen FEHLER für OpenAI-Felder geben
        EditorValidationInput input = new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "10", "500", "60",
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "", EffectiveApiKeyDescriptor.absent(), "");

        EditorValidationReport report = validator.validate(input);

        // OpenAI-Felder dürfen keinen Fehler produzieren (Provider nicht aktiv)
        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_OPENAI_MODEL))
                .noneMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_OPENAI_TIMEOUT))
                .noneMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
    }

    // =========================================================================
    // max.title.length
    // =========================================================================

    private static EditorValidationInput inputWithMaxTitleLength(String rawMaxTitleLength) {
        return new EditorValidationInput(
                "claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
                "3", "10", "500", rawMaxTitleLength,
                "https://api.anthropic.com", "claude-3-5-sonnet", "30",
                EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
                "", "", "30", EffectiveApiKeyDescriptor.absent(), "");
    }

    @Test
    void validate_maxTitleLength_empty_producesError() {
        EditorValidationReport report = validator.validate(inputWithMaxTitleLength(""));

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_TITLE_LENGTH))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
    }

    @Test
    void validate_maxTitleLength_nonInteger_producesError() {
        EditorValidationReport report = validator.validate(inputWithMaxTitleLength("abc"));

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_TITLE_LENGTH))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
    }

    @Test
    void validate_maxTitleLength_tooSmall_producesError() {
        EditorValidationReport report = validator.validate(inputWithMaxTitleLength("5"));

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_TITLE_LENGTH))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
    }

    @Test
    void validate_maxTitleLength_nine_producesError() {
        EditorValidationReport report = validator.validate(inputWithMaxTitleLength("9"));

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_TITLE_LENGTH))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
    }

    @Test
    void validate_maxTitleLength_tooLarge_producesError() {
        EditorValidationReport report = validator.validate(inputWithMaxTitleLength("121"));

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_TITLE_LENGTH))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.ERROR);
    }

    @Test
    void validate_maxTitleLength_lowWarnRange_producesWarning() {
        EditorValidationReport report = validator.validate(inputWithMaxTitleLength("15"));

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_TITLE_LENGTH))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.WARNING);
    }

    @Test
    void validate_maxTitleLength_highWarnRange_producesWarning() {
        EditorValidationReport report = validator.validate(inputWithMaxTitleLength("110"));

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_TITLE_LENGTH))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.WARNING);
    }

    @Test
    void validate_maxTitleLength_sixty_producesNoFinding() {
        EditorValidationReport report = validator.validate(inputWithMaxTitleLength("60"));

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_TITLE_LENGTH))
                .isEmpty();
    }

    @Test
    void validate_maxTitleLength_twenty_producesWarning() {
        EditorValidationReport report = validator.validate(inputWithMaxTitleLength("20"));

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_TITLE_LENGTH))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.WARNING);
    }

    @Test
    void validate_maxTitleLength_thirtyNine_producesWarning() {
        EditorValidationReport report = validator.validate(inputWithMaxTitleLength("39"));

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_TITLE_LENGTH))
                .anyMatch(f -> f.severity() == EditorValidationSeverity.WARNING);
    }

    @Test
    void validate_maxTitleLength_forty_producesNoFinding() {
        EditorValidationReport report = validator.validate(inputWithMaxTitleLength("40"));

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_TITLE_LENGTH))
                .isEmpty();
    }

    @Test
    void validate_maxTitleLength_ninetyNine_producesNoFinding() {
        EditorValidationReport report = validator.validate(inputWithMaxTitleLength("99"));

        assertThat(report.findingsFor(EditorConfigurationValidator.FIELD_MAX_TITLE_LENGTH))
                .isEmpty();
    }
}
