import json
from pathlib import Path

import pytest


ROOT = Path(__file__).resolve().parents[2]
APP_DIR = ROOT / "web" / "operator-gui"
INDEX = APP_DIR / "index.html"
STYLES = APP_DIR / "styles.css"
APP_JS = APP_DIR / "app.js"
OPERATOR_LABELS_JS = APP_DIR / "operator-labels.js"
SUBMISSION_IMPORT_JS = APP_DIR / "submission-import.js"
EVIDENCE_GUIDANCE_JS = APP_DIR / "evidence-guidance.js"
OPERATOR_SEARCH_JS = APP_DIR / "operator-search.js"
PITCH = APP_DIR / "pitch.html"
PITCH_STYLES = APP_DIR / "pitch.css"
PITCH_JS = APP_DIR / "pitch.js"
UI_OVERHAUL_FINAL_RESULTS = ROOT / "data" / "logs" / "ui-overhaul-final-results.json"
UI_OVERHAUL_FINAL_SCREENSHOTS = [
    "ui-overhaul-desktop-final-queue.png",
    "ui-overhaul-desktop-final-workbench-queries.png",
    "ui-overhaul-desktop-final-knowledge-db.png",
    "ui-overhaul-desktop-final-knowledge-corrections.png",
    "ui-overhaul-mobile-final-queue.png",
    "ui-overhaul-mobile-final-workbench-queries.png",
    "ui-overhaul-mobile-final-knowledge-db.png",
    "ui-overhaul-mobile-final-knowledge-corrections.png",
]


def _read(path: Path) -> str:
    return path.read_text(encoding="utf-8")


def test_operator_workbench_files_exist():
    assert INDEX.exists()
    assert STYLES.exists()
    assert APP_JS.exists()
    assert OPERATOR_LABELS_JS.exists()
    assert SUBMISSION_IMPORT_JS.exists()
    assert EVIDENCE_GUIDANCE_JS.exists()
    assert OPERATOR_SEARCH_JS.exists()


def test_operator_gui_text_does_not_include_mojibake_fragments():
    suspect_fragments = [
        "Â",
        "Ã",
        "�",
        "ê",
        "ë",
        "ì",
        "í",
        "?",
        "?¤",
        "?",
        "?´",
        "?¸",
        "?",
        "?¬",
        "?",
        "?",
    ]

    for path in [INDEX, APP_JS, OPERATOR_LABELS_JS, SUBMISSION_IMPORT_JS, EVIDENCE_GUIDANCE_JS, OPERATOR_SEARCH_JS]:
        text = _read(path)
        for fragment in suspect_fragments:
            assert fragment not in text, f"{path.name} contains mojibake fragment {fragment!r}"


def test_pitch_page_presents_copyright_review_flow_with_real_captures():
    html = _read(PITCH)
    styles = _read(PITCH_STYLES)
    script = _read(PITCH_JS)

    for path in [PITCH, PITCH_STYLES, PITCH_JS]:
        assert path.exists()

    for required_text in [
        "이미지 저작권 심사를",
        "판별 방식",
        "운영 화면",
        "DB 성장",
        "거버넌스",
        "Mermaid 원본 보기",
        "실제 9500 운영 콘솔",
        "Google",
        "Naver",
        "Ollama",
    ]:
        assert required_text in html

    for asset_name in [
        "case-review.png",
        "evidence-search.png",
        "knowledge-db.png",
        "provider-controls.png",
        "risk-pipeline.svg",
        "decision-loop.svg",
    ]:
        assert (APP_DIR / "pitch-assets" / asset_name).exists()
        assert f"pitch-assets/{asset_name}" in html

    assert "hydrateMetrics" in script
    assert "overflow-x: hidden" in styles
    assert "@media (max-width: 640px)" in styles


def test_workbench_shell_exposes_all_internal_operator_views():
    html = _read(INDEX)
    styles = _read(STYLES)

    assert "<nav" in html
    assert "<main" in html
    assert 'data-internal-only="true"' in html
    assert 'class="product-purpose"' in html
    assert 'aria-label="제품 목적"' in html
    assert "이미지 저작권 위험 심사" in html
    assert "제출 이미지, 외부 검색 근거, 내부 기준 DB를 한 화면에서 검토합니다." in html
    assert ".product-purpose" in styles
    assert 'class="operator-workflow"' in html
    assert 'aria-label="운영 흐름"' in html
    assert "심사 건 추가" in html
    assert "근거 보강" in html
    assert "운영 결정" in html
    assert "사진 추가 또는 제출 폴더 불러오기" in html
    assert "추천 쿼리로 외부 검색 결과를 보강합니다." in html
    assert "기준 DB에 반영합니다." in html
    assert ".operator-workflow" in styles
    assert "bulk-approve" not in html.lower()
    assert "bulk-reject" not in html.lower()

    assert 'data-view="evidence"' not in html
    assert 'id="evidence-view"' not in html
    assert 'data-view="corrections"' not in html
    assert 'id="corrections-view"' not in html

    for view in [
        "queue",
        "workbench",
        "knowledge",
        "providers",
        "audit",
    ]:
        assert f'data-view="{view}"' in html
        assert f'id="{view}-view"' in html


def test_static_app_models_core_review_operations():
    script = _read(APP_JS)

    for state_name in [
        "submissions",
        "providers",
        "knowledgeEntries",
        "collectionCandidates",
        "corrections",
        "auditEvents",
    ]:
        assert state_name in script

    for renderer in [
        "renderQueue",
        "renderCaseReview",
        "renderEvidenceSearch",
        "renderKnowledgeBase",
        "renderCollectionCandidates",
        "renderCorrections",
        "renderProviderControls",
        "renderAuditLog",
        "renderCoverageTabs",
    ]:
        assert f"function {renderer}" in script

    for operation in [
        "selectCase",
        "setDecision",
        "runManualSearch",
        "rerunEnrichment",
        "addKnowledgeEntry",
        "runCandidateCollection",
        "promoteCollectionCandidate",
        "deactivateCorrectionEntry",
        "toggleProvider",
    ]:
        assert f"function {operation}" in script


def test_operator_gui_extracts_submission_import_helpers_from_main_app():
    html = _read(INDEX)
    script = _read(APP_JS)
    labels = _read(OPERATOR_LABELS_JS)
    helpers = _read(SUBMISSION_IMPORT_JS)
    guidance = _read(EVIDENCE_GUIDANCE_JS)
    search = _read(OPERATOR_SEARCH_JS)

    assert html.index('src="operator-labels.js"') < html.index('src="submission-import.js"')
    assert html.index('src="submission-import.js"') < html.index('src="app.js"')
    assert html.index('src="evidence-guidance.js"') < html.index('src="app.js"')
    assert html.index('src="operator-search.js"') < html.index('src="app.js"')
    assert "OperatorLabels" in labels
    assert "riskLabels" in labels
    assert "providerLabels" in labels
    assert "} = window.OperatorLabels;" in script
    assert "OperatorSubmissionImport" in helpers
    assert "fileToImagePayload" in helpers
    assert "importedFolderStatusMessage" in helpers
    assert "importedSubmissionStatusMessage" in helpers
    assert "window.OperatorSubmissionImport.fileToImagePayload(file)" in script
    assert "window.OperatorSubmissionImport.importedFolderStatusMessage" in script
    assert "window.OperatorSubmissionImport.importedSubmissionStatusMessage" in script
    assert "OperatorEvidenceGuidance" in guidance
    assert "suggestedEvidenceQueries" in guidance
    assert "evidenceFollowupReasons" in guidance
    assert "window.OperatorEvidenceGuidance.suggestedEvidenceQueries" in script
    assert "window.OperatorEvidenceGuidance.evidenceFollowupReasons" in script
    assert "OperatorSearch" in search
    assert "formatQueryStatus" in search
    assert "formatQueryStrategy" in search
    assert "normalizeManualSearchProvider" in search
    assert "window.OperatorSearch.formatQueryStatus" in script
    assert "window.OperatorSearch.formatQueryStrategy" in script
    assert "window.OperatorSearch.normalizeManualSearchProvider" in script


def test_operator_gui_does_not_boot_with_demo_review_data():
    script = _read(APP_JS)

    for collection in [
        "submissions",
        "providers",
        "knowledgeEntries",
        "collectionCandidates",
        "corrections",
        "auditEvents",
    ]:
        assert f"const {collection} = [];" in script

    for demo_token in [
        "SUB-1007",
        "ev-1007",
        "kb-iu",
        "DEC-0992",
    ]:
        assert demo_token not in script

    assert "clearRuntimeData()" in script
    assert "renderNoSelectedCase" in script
    assert "state.apiError" in script


def test_design_contract_has_accessibility_risk_states_and_responsive_layouts():
    styles = _read(STYLES)

    assert ":focus-visible" in styles
    assert "@media (max-width: 980px)" in styles
    assert "@media (max-width: 680px)" in styles
    assert "linear-gradient" not in styles.lower()
    assert "orb" not in styles.lower()

    for class_name in [
        ".risk-high",
        ".risk-medium",
        ".risk-low",
        ".risk-failed",
        ".source-naver",
        ".source-google",
        ".source-llm",
        ".source-internal",
    ]:
        assert class_name in styles


@pytest.mark.skipif(
    not UI_OVERHAUL_FINAL_RESULTS.exists(),
    reason="local-only audit artifacts: data/ is gitignored, so fresh clones lack them; "
    "regenerate with the live-server Playwright audit before release sign-off",
)
def test_ui_overhaul_final_audit_has_no_overflow_and_required_screenshots():
    results = json.loads(_read(UI_OVERHAUL_FINAL_RESULTS))

    for breakpoint_results in results.values():
        for view_name, view_result in breakpoint_results.items():
            if not isinstance(view_result, dict) or "overflow" not in view_result:
                continue

            assert view_result["docW"] <= view_result["vw"], view_name
            assert view_result["overflow"] == [], view_name

    for screenshot_name in UI_OVERHAUL_FINAL_SCREENSHOTS:
        screenshot_path = UI_OVERHAUL_FINAL_RESULTS.parent / screenshot_name
        assert screenshot_path.exists(), screenshot_name
        assert screenshot_path.stat().st_size > 0, screenshot_name


def test_header_coverage_uses_horizontal_filter_tabs_not_scrolling_panel():
    html = _read(INDEX)
    styles = _read(STYLES)
    script = _read(APP_JS)

    assert 'id="coverage-tabs"' in html
    assert 'id="search-coverage"' not in html
    assert "function renderCoverageTabs" in script
    assert "function applyCoverageFilter" in script
    assert "data-coverage-filter" in script
    assert ".coverage-tabs" in styles
    assert ".coverage-tab" in styles
    assert ".search-coverage" not in styles


def test_queue_uses_grid_row_contract_for_dense_evidence_and_provider_columns():
    html = _read(INDEX)
    styles = _read(STYLES)
    script = _read(APP_JS)

    assert "queue-grid" in html
    assert "queue-row" in script
    assert ".queue-grid" in styles
    assert ".queue-row" in styles
    assert "grid-template-columns: 28px 64px minmax(104px, 0.68fr) 72px minmax(126px, 0.58fr) minmax(360px, 1.5fr) 82px 76px 90px" in styles
    assert "queue-submission-cell" in script
    assert "queue-risk-cell" in script
    assert "queue-time-cell" in script
    assert "queue-title" not in script


def test_visual_overhaul_removes_obsolete_header_cards_and_fixed_table_widths():
    styles = _read(STYLES)

    for obsolete_selector in [
        ".coverage-main",
        ".coverage-badges",
        ".coverage-provider-grid",
        ".coverage-mini-line",
    ]:
        assert obsolete_selector not in styles

    assert ".queue-table th:nth-child" not in styles
    assert ".queue-row td:nth-child(5)" in styles
    assert ".queue-row td:nth-child(6)" in styles


def test_case_workbench_owns_evidence_and_query_history_as_internal_tabs():
    html = _read(INDEX)
    script = _read(APP_JS)
    styles = _read(STYLES)

    assert 'id="workbench-view"' in html
    assert 'data-workbench-tab="summary"' not in html
    assert 'data-workbench-tab="evidence"' in html
    assert 'data-workbench-tab="queries"' in html
    assert 'data-workbench-tab="decision"' not in html
    assert 'data-workbench-panel="evidence"' in html
    assert 'data-workbench-panel="queries"' in html
    assert 'data-workbench-panel="decision"' not in html
    assert "근거 및 판단" in html
    assert 'id="query-history"' in html
    assert 'id="search-results"' in html
    assert "workbenchTab" in script
    assert 'workbenchTab: "evidence"' in script
    assert "function switchWorkbenchTab" in script
    assert 'switchView("workbench")' in script
    assert 'panelName === nextTab ||' not in script
    assert ".workbench-tabs" in styles
    assert ".workbench-panel" in styles


def test_case_workbench_tabs_do_not_share_the_same_review_layout():
    html = _read(INDEX)

    evidence_panel = html.split('data-workbench-panel="evidence"', 1)[1].split('data-workbench-panel="queries"', 1)[0]
    query_panel = html.split('data-workbench-panel="queries"', 1)[1].split('id="knowledge-view"', 1)[0]

    assert 'id="case-image"' in evidence_panel
    assert 'id="evidence-groups"' in evidence_panel
    assert evidence_panel.index('id="recommendation-box"') < evidence_panel.index('id="case-image"')
    assert evidence_panel.index('id="decision-memo"') < evidence_panel.index('id="case-image"')
    assert 'id="query-history"' in query_panel


def test_case_decision_controls_float_while_reviewing_evidence():
    html = _read(INDEX)
    script = _read(APP_JS)
    styles = _read(STYLES)

    assert "floating-decision-panel" in html
    assert 'id="floating-case-score"' in html
    assert "floating-case-score" in script
    assert ".floating-decision-panel" in styles
    assert "position: fixed" in styles
    assert "bottom: 24px" in styles
    assert 'data-workbench-panel="evidence"' in styles
    assert "padding-right: 334px" in styles
    assert ".evidence-layout" in styles
    assert "padding-bottom: 260px" in styles
    assert ".floating-decision-panel .decision-actions" in styles


def test_knowledge_base_rows_separate_title_metadata_chips_and_actions():
    script = _read(APP_JS)
    styles = _read(STYLES)

    assert "knowledge-main" in script
    assert "knowledge-chip-row" in script
    assert "knowledge-detail-line" in script
    assert "memoInline" in script
    assert "memoBlock" in script
    assert "knowledge-meta" in script
    assert ".knowledge-main" in styles
    assert ".knowledge-chip-row" in styles
    assert ".knowledge-detail-line" in styles
    assert ".knowledge-meta" in styles
    assert ".knowledge-actions" in styles
    assert "grid-template-areas" in styles


def test_correction_history_lives_inside_knowledge_database_panel():
    html = _read(INDEX)
    script = _read(APP_JS)
    styles = _read(STYLES)

    assert 'data-knowledge-tab="collect"' in html
    assert 'data-knowledge-tab="registered"' in html
    assert 'data-knowledge-tab="manual"' in html
    assert 'data-knowledge-tab="corrections"' in html
    assert 'data-knowledge-panel="collect"' in html
    assert 'data-knowledge-panel="registered"' in html
    assert 'data-knowledge-panel="manual"' in html
    assert 'data-knowledge-panel="corrections"' in html
    assert 'id="corrections-list"' in html
    assert "knowledgeTab" in script
    assert "function switchKnowledgeTab" in script
    assert 'switchView("knowledge")' in script
    assert 'switchKnowledgeTab("corrections")' in script
    assert ".knowledge-tabs" in styles
    assert ".knowledge-panel" in styles


def test_knowledge_collection_and_registered_entries_are_separate_tabs():
    html = _read(INDEX)

    collect_panel = html.split('data-knowledge-panel="collect"', 1)[1].split('data-knowledge-panel="registered"', 1)[0]
    registered_panel = html.split('data-knowledge-panel="registered"', 1)[1].split('data-knowledge-panel="manual"', 1)[0]
    manual_panel = html.split('data-knowledge-panel="manual"', 1)[1].split('data-knowledge-panel="corrections"', 1)[0]

    assert 'id="candidate-collection-form"' in collect_panel
    assert 'id="collection-candidates"' in collect_panel
    assert 'id="knowledge-list"' not in collect_panel
    assert 'id="knowledge-list"' in registered_panel
    assert 'id="candidate-collection-form"' not in registered_panel
    assert 'id="knowledge-form"' in manual_panel


def test_safety_rules_are_visible_in_ui_contract():
    html = _read(INDEX)
    script = _read(APP_JS)

    assert 'id="decision-memo"' in html
    assert 'id="manual-query-provider"' in html
    # 2026-06-12 안전 규칙 개정(설계 승인 2026-06-11): 운영자 수동 검색에
    # 구글 근거 검색(텍스트 쿼리)을 동적 옵션으로 노출한다. 정적 HTML 기본값은
    # 네이버만 유지하고, 이미지 업로드 역검색 금지는 그대로다.
    assert '{ id: "google_search", label: providerLabels.google_search }' in script
    assert "네이버" in html
    assert "reverse search" not in html.lower()
    assert "sourceEvidenceIds" in script
    assert "requiresMemo" in script
    assert "automatic" in script


def test_provider_quota_zero_does_not_render_nan_meter():
    script = _read(APP_JS)

    assert "provider.quota ? Math.min" in script
    assert "configuredEnv" in script
    assert "requiredEnv" in script


def test_query_history_rerun_preserves_original_search_provider():
    script = _read(APP_JS)
    search = _read(OPERATOR_SEARCH_JS)

    assert "data-rerun-provider" in script
    assert "rerunHistoricalQuery" in script
    assert "normalizeManualSearchProvider" in search
    assert "window.OperatorSearch.normalizeManualSearchProvider" in script
    assert 'providerSelect.value = provider' in script
    assert "NaN" not in script


def test_operator_gui_exposes_submission_reload_action():
    html = _read(INDEX)
    script = _read(APP_JS)

    assert 'id="submission-folder"' in html
    assert 'id="submission-image"' in html
    assert 'id="submission-image-name"' in html
    assert 'id="queue-upload-guidance"' in html
    assert 'id="upload-submission-image"' in html
    assert 'id="reload-submissions"' in html
    assert 'id="submission-import-status"' in html
    assert "사진을 추가하면 현재 큐에 새 심사 건으로 들어가고" in html
    assert "/api/submissions/reload" in script
    assert "/api/submissions/import-folder" in script
    assert "/api/submissions/upload-image" in script
    assert "uploadSubmissionImage" in script
    assert "updateSubmissionImageName" in script
    assert 'switchView("workbench")' in script
    assert 'switchWorkbenchTab("evidence")' in script
    assert "새 심사 건으로 바로 선택했습니다." in script
    assert "reloadSubmissions" in script


def test_operator_gui_uses_clear_korean_status_copy_for_queue_imports():
    script = _read(APP_JS) + _read(SUBMISSION_IMPORT_JS)

    for required_copy in [
        "제출 폴더를 읽는 중입니다.",
        "추가된 제출 없음",
        "건 가져옴",
        "건 추가됨",
        "선택됨",
        "재분석할 제출이 없습니다.",
        "건 재분석을 시작합니다.",
        "재분석 중",
        "건 재분석 완료",
        "사진을 현재 제출 폴더에 넣는 중입니다.",
        "사진이 추가되었습니다.",
    ]:
        assert required_copy in script


def test_operator_gui_bulk_rerun_uses_checked_queue_rows():
    script = _read(APP_JS)

    assert "function selectedBulkSubmissionIds" in script
    assert 'input[data-bulk-id]:checked' in script
    assert "function rerunSelectedEnrichment" in script
    assert 'document.getElementById("bulk-rerun").addEventListener("click", rerunSelectedEnrichment)' in script


def test_operator_gui_manual_knowledge_entry_accepts_reference_image():
    html = _read(INDEX)
    script = _read(APP_JS)

    assert 'id="knowledge-image"' in html
    assert 'id="knowledge-image-name"' in html
    assert 'type="file"' in html
    assert 'accept="image/*"' in html
    assert "updateKnowledgeImageName" in script
    assert "readKnowledgeImage" in script
    assert "/api/knowledge/manual" in script
    assert "imageAsset" in script
    assert "sampleFingerprints" in script


def test_operator_gui_exposes_keyword_candidate_collection_workflow():
    html = _read(INDEX)
    script = _read(APP_JS)
    search = _read(OPERATOR_SEARCH_JS)

    assert 'id="candidate-collection-form"' in html
    assert 'id="collection-query"' in html
    assert 'id="collection-provider"' in html
    assert 'option value="google_search"' not in html
    assert 'id="collection-candidates"' in html
    assert 'id="collection-status"' in html
    assert 'id="select-all-candidates"' in html
    assert 'id="clear-selected-candidates"' in html
    assert "/api/collections/keyword" in script
    assert "/api/collections/candidates/" in script
    assert "/api/collections/candidates/promote-batch" in script
    assert "바로 편입" in script
    assert "선택 후보 묶어서 편입" in html
    assert 'id="collection-promotion-form"' in html
    assert 'id="collection-promotion-name"' in html
    assert 'id="collection-promotion-type"' in html
    assert 'id="collection-promotion-aliases"' in html
    assert 'id="collection-promotion-keywords"' in html
    assert 'id="collection-promotion-memo"' in html
    assert 'id="promote-selected-candidates"' in html
    assert "selectedCollectionCandidateIds" in script
    assert "setAllCollectionCandidateSelection" in script
    assert "promoteSelectedCollectionCandidates" in script
    assert "clearCollectionCandidatesForSearch" in script
    assert "clearCollectionCandidatesAfterAction" in script
    assert "dismissCollectionCandidate" in script
    assert "currentCollectionQuery" in script
    assert "visibleCollectionCandidates" in script
    assert "data-collection-candidate-id" in script
    assert "data-promote-candidate" in script
    assert "data-dismiss-candidate" in script
    assert "candidate-actions" in script
    assert ".candidate-actions" in _read(STYLES)
    assert "candidate-card" in script
    assert "sourceClass(candidate.provider)" in script
    assert "formatCandidateSourceType" in script
    assert "검색 이미지 결과" in script
    assert "페이지 대표 이미지" in script
    assert "formatQueryStrategy" in search
    assert "window.OperatorSearch.formatQueryStrategy" in script
    assert "구글 페이지 제목 기반" in search
    assert "제출 제목/파일명 기반" in search


def test_google_custom_search_is_exposed_as_operator_text_query_choice():
    html = _read(INDEX)
    script = _read(APP_JS)
    search = _read(OPERATOR_SEARCH_JS)
    labels = _read(OPERATOR_LABELS_JS)

    # 정적 HTML은 네이버 기본값만 두고 부트스트랩 후 동적으로 채운다.
    assert 'value="google_search"' not in html
    assert '{ id: "google_search", label: providerLabels.google_search }' in script
    assert "구글 근거 검색" in labels
    assert 'provider === "google_search" ? "google_search" : "naver"' in search
    assert "reverse search" not in html.lower()
    assert "operatorSearchProviders" in script
    assert "visibleProviderControls" in script
    # 검색 선택지뿐 아니라 제공자 카드/상태 칩에서도 google_search가 보여야 한다.
    assert "retiredProviderIds = new Set()" in script


def test_audit_target_and_change_columns_have_equal_widths():
    styles = _read(STYLES)

    assert "--audit-object-width: 24%" in styles
    assert ".audit-table th:nth-child(4)" in styles
    assert ".audit-table th:nth-child(5)" in styles
    assert "width: var(--audit-object-width)" in styles


def test_queue_provider_judgments_render_on_one_line_with_narrower_reason_column():
    script = _read(APP_JS)
    styles = _read(STYLES)

    assert "provider-strip" in script
    assert "queue-provider-chip" in script
    assert "formatQueueProviderStatus" in script
    assert "근거 있음" in script
    assert "결과 없음" in script
    assert "미실행" in script
    assert ".queue-provider-strip" in styles
    assert "flex-wrap: nowrap" in styles
    assert ".queue-row td:nth-child(6)" in styles
    assert "white-space: nowrap" in styles
    assert "grid-template-columns: 28px 64px minmax(104px, 0.68fr) 72px minmax(126px, 0.58fr) minmax(360px, 1.5fr) 82px 76px 90px" in styles


def test_evidence_operator_status_actions_are_binary_use_or_ignore():
    script = _read(APP_JS)

    assert '["used_for_judgment", "사용"]' in script
    assert '["ignored", "미사용"]' in script
    assert '["irrelevant",' not in script
    assert '["false_positive",' not in script
    assert '["pending",' not in script
    assert "normalizeEvidenceOperatorStatus" in script


def test_operator_gui_uses_korean_operator_copy_for_visible_labels():
    html = _read(INDEX)
    script = _read(APP_JS)

    for english_label in [
        "Image Rights Operator Console",
        "Review Queue",
        "Case Review",
        "Evidence Search",
        "Knowledge Base",
        "Provider Controls",
        "Audit Log",
        "Automated Recommendation",
        "Fingerprint match",
        "Face/person",
        "LLM summary",
        "Provider failure",
        "Google Web Detection</option>",
    ]:
        assert english_label not in html

    assert "권리 검수 콘솔" in html
    assert "심사 대기열" in html
    assert "얼굴/인물 감지" in html
    assert "공급자" not in html
    assert "외부 검색 tool 활용" in html
    assert "공급자" not in script
    assert "외부 검색 tool" in script
    assert "formatEvidenceTitle" in script
    assert "formatReason" in script
    assert "동일인 판정이 아닙니다" in script
    assert "신뢰도" in script
    assert "근거 ID" in script
    assert "샘플 지문" in script


def test_operator_gui_renders_search_result_links_and_thumbnails():
    script = _read(APP_JS)

    assert "renderEvidenceLink" in script
    assert "thumbnailUrl" in script
    assert "matchType" in script
    assert "evidence-preview" in script
    assert "search_result_image" in script
    assert "search_result_page_image" in script
    assert "Naver blog search result found" in script
    assert "naver_blog" in script
    assert "Naver web search result found" in script
    assert "naver_web" in script
    assert "Google custom image search result found" in script
    assert "Google custom web search result found" in script
    assert "google_search" in script
    assert "google_best_guess" in script
    assert "google_face_crop_page" in script
    assert "google_face_crop_entity" in script
    assert "searchType" in script
    assert "이미지 유사도" in script


def test_operator_gui_prioritizes_evidence_and_hides_overflow_details():
    script = _read(APP_JS)
    styles = _read(STYLES)

    assert "renderEvidenceSummary" in script
    assert "renderEvidenceGroup" in script
    assert "topItems" in script
    assert "자세히 보기" in script
    assert "renderEvidenceGroups({ ...submission, evidence: searchableEvidence })" in script
    assert "evidence-summary-board" in script
    assert ".evidence-card-grid" in styles
    assert ".evidence-details" in styles


def test_operator_gui_suggests_followup_queries_for_insufficient_evidence():
    html = _read(INDEX)
    script = _read(APP_JS)
    guidance = _read(EVIDENCE_GUIDANCE_JS)
    combined_script = script + guidance
    styles = _read(STYLES)

    assert 'id="evidence-next-actions"' in html
    assert "function evidenceNeedsFollowup" in combined_script
    assert "function evidenceFollowupReasons" in combined_script
    assert "function suggestedEvidenceQueries" in combined_script
    assert "function renderEvidenceNextActions" in script
    assert "function applySuggestedQuery" in script
    assert "data-suggested-query" in script
    assert "evidence-followup-reasons" in script
    assert "직접 매칭 또는 원문 페이지 근거가 없습니다." in guidance
    assert "검색 근거가 2건 미만입니다." in guidance
    assert "외부 검색 tool이 빈 결과를 반환했습니다." in guidance
    assert "외부 검색 tool 실패 이력이 있습니다." in guidance
    assert "switchWorkbenchTab(\"queries\")" in script
    assert "/api/search/manual" in script
    assert "추천 쿼리를 입력했습니다" in script
    assert ".evidence-next-action-panel" in styles
    assert ".evidence-followup-reasons" in styles
    assert ".suggested-query-list" in styles


def test_operator_gui_separates_face_crop_web_evidence_from_identity_matching():
    script = _read(APP_JS)

    assert "faceCropSearch" in script
    assert "face_web" in script
    assert "얼굴 영역 웹 근거" in script
    assert "동일인 판정이 아닙니다" in script


def test_operator_gui_exposes_evidence_status_and_watchlist_controls():
    script = _read(APP_JS)
    styles = _read(STYLES)

    assert "/api/evidence/" in script
    assert "data-evidence-status" in script
    assert "판단에 사용" in script
    assert "오탐" in script
    assert "주의 후보 근거" in script
    assert "knowledgeEntryStatus" in script
    assert "/promote-watchlist" in script
    assert "/exclude-watchlist" in script
    assert "data-promote-watchlist" in script
    assert "data-exclude-watchlist" in script
    assert ".watchlist-chip" in styles


def test_visual_assets_are_referenced_for_review_images():
    html = _read(INDEX)
    assets_dir = APP_DIR / "assets"

    assert (assets_dir / "case-portrait.svg").exists()
    assert "assets/case-portrait.svg" in html or "assets/case-portrait.svg" in _read(APP_JS)

    for orphan_asset in [
        "case-character.svg",
        "case-emblem.svg",
        "match-web.svg",
    ]:
        assert not (assets_dir / orphan_asset).exists(), f"{orphan_asset} is unused; do not reintroduce it"


def test_suggested_queries_support_one_click_and_batch_execution():
    script = _read(APP_JS)
    styles = _read(STYLES)

    assert "executeSuggestedQueries" in script
    assert "data-run-suggested-query" in script
    assert 'id="run-all-suggested-queries"' in script
    assert "모두 실행" in script
    assert "바로 실행" in script
    assert "suggested-query-run-status" in script
    # 기존 "입력칸 채우기" 버튼은 수정용으로 유지된다.
    assert "data-suggested-query" in script
    assert ".suggested-query-item" in styles


def test_registered_knowledge_panel_supports_search_filter_edit_and_lifecycle():
    html = _read(INDEX)
    script = _read(APP_JS)

    registered_panel = html.split('data-knowledge-panel="registered"', 1)[1].split('data-knowledge-panel="manual"', 1)[0]
    assert 'id="knowledge-search"' in registered_panel
    assert 'id="knowledge-type-filter"' in registered_panel
    assert 'id="knowledge-status-filter"' in registered_panel
    assert 'id="knowledge-active-filter"' in registered_panel

    assert "filteredKnowledgeEntries" in script
    assert "data-edit-kb" in script
    assert "data-knowledge-edit-form" in script
    assert "saveKnowledgeEntryEdit" in script
    assert "data-deactivate-kb" in script
    assert "data-reactivate-kb" in script
    assert "재활성 사유 메모" in script
    assert "data-toggle-kb" not in script


def test_workbench_shows_detected_face_crop_strip():
    html = _read(INDEX)
    script = _read(APP_JS)

    assert 'id="face-crop-strip"' in html
    assert "faceCrops" in script
    assert "얼굴 영역" in script
    # 면책 문구는 기존 증거 행과 동일한 표현을 재사용한다.
    assert "동일 인물 판정은 아닙니다" in script
    # 크롭 썸네일은 클릭해 원본 크롭을 크게 볼 수 있어야 한다.
    assert "face-crop-link" in script


def test_rerun_diff_summary_and_new_evidence_badges_are_rendered():
    html = _read(INDEX)
    script = _read(APP_JS)
    styles = _read(STYLES)

    assert 'id="rerun-diff-summary"' in html
    assert "renderRerunDiffSummary" in script
    assert "lastRerunDiff" in script
    assert "evidence-new-chip" in script
    assert "이번 재분석에서 제거됨" in script
    assert ".evidence-new-chip" in styles
    assert ".evidence-row-new" in styles
