import stat from pathlib import Path ROOT = Path(__file__).resolve().parents[1] def test_static_assets_are_container_readable(): unreadable = [] for asset in (ROOT / "web/static").rglob("*"): if asset.is_file() and not (asset.stat().st_mode & stat.S_IROTH): unreadable.append(str(asset.relative_to(ROOT))) assert unreadable == [] def test_frontend_v2_shell_assets_exist_and_are_indexed(): assert (ROOT / "web/static/css/ewoooc-tokens.css").exists() assert (ROOT / "web/static/css/ewoooc-shell.css").exists() assert (ROOT / "templates/ewoooc_base.html").exists() assert (ROOT / "templates/components/_ewoooc_shell.html").exists() agents = (ROOT / "AGENTS.md").read_text(encoding="utf-8") constitution = (ROOT / "CONSTITUTION.md").read_text(encoding="utf-8") roadmap = (ROOT / "docs/guides/frontend_upgrade_roadmap.md").read_text(encoding="utf-8") assert "docs/guides/frontend_upgrade_roadmap.md" in agents assert "前端 V2 視覺基準" in constitution assert "禁止用 mock data" in roadmap def test_frontend_v2_shell_uses_real_runtime_context(): shell = (ROOT / "templates/components/_ewoooc_shell.html").read_text(encoding="utf-8") base = (ROOT / "templates/ewoooc_base.html").read_text(encoding="utf-8") app_source = (ROOT / "app.py").read_text(encoding="utf-8") config_source = (ROOT / "config.py").read_text(encoding="utf-8") constitution = (ROOT / "CONSTITUTION.md").read_text(encoding="utf-8") assert "scheduler_stats" in shell assert "session.get('username')" in shell assert "next_run|default(None)" in shell assert "components/_ewoooc_shell.html" in base assert "'webcrumbs_config': {" in app_source assert "WEBCRUMBS_RUNTIME_URL" in config_source assert "/webcrumbs-assets/loader/webcrumbs-compatible-loader.js" in config_source assert "WEBCRUMBS_ASSET_UPSTREAM_URL" in config_source assert "data-webcrumbs-runtime" in base assert 'name="ui" value="v2"' not in base assert "前端文案與工作溝通隔離" in constitution assert "不得搬到使用者可見頁面" in constitution forbidden_markers = [ "mockProducts", "mockData", "fake", "假商品", "假 KPI", ] combined = shell + "\n" + base assert all(marker not in combined for marker in forbidden_markers) def test_high_visibility_pages_use_traditional_chinese_labels(): page_paths = [ "templates/code_review.html", "templates/ai_automation_smoke.html", "templates/login.html", "templates/admin/budget.html", "templates/admin/agent_orchestration.html", "templates/admin/ppt_audit_history.html", "templates/admin/host_health.html", "templates/dashboard_v2.html", "templates/components/_navbar.html", "templates/cicd_dashboard.html", ] combined = "\n".join((ROOT / path).read_text(encoding="utf-8") for path in page_paths) assert "部署守門與程式碼審查" in combined assert "等待程式碼審查完成" in combined assert "AI 自動化健康檢查" in combined assert "AI 閉環守門" in combined assert "產線健康度" in combined assert "工作隊列" in combined assert "覆蓋率流程" in combined assert "同步部署" in combined assert "服務更新監控" in combined assert "最新更新流程" in combined assert "執行環境正常" in combined assert "視覺檢查" in combined assert "下載檢查紀錄" in combined assert "健康檢查服務" in combined assert "資料服務" in combined assert "Top 5 成本使用情境" in combined assert "工具協作 × 使用情境" in combined forbidden_visible_text = [ "工作視窗", "Codex", "Claude", "推到 Gitea", "本輪已完成", "剛剛修正", "後續 session", "JSONL", "健康檢查 API", "JSON.stringify(item.details", "PostgreSQL", "CSRF 防護", "Session 2h", "燒錢呼叫端", "{{ m.caller }}", "{{ m.server }}", "AI Code Review", "Smoke Dashboard", "FOUR-AGENT CONTROL PLANE", "WarningCommit", "Branch", "${f.severity}", "ea.priority.toUpperCase", "CI/CD Dashboard", ">最新 Pipeline<", "暫無 Pipeline", "開啟 GitLab Pipelines", ">Runtime 狀態:<", "Docker Compose runtime", "Runtime 正常", ">Runtime<", "Vision QA", ] for marker in forbidden_visible_text: assert marker not in combined def test_topbar_observability_indicator_is_cached_and_timeout_bounded(): base_js = (ROOT / "web/static/js/ewoooc-base.js").read_text(encoding="utf-8") observability_route = (ROOT / "routes/admin_observability_routes.py").read_text(encoding="utf-8") assert "momoObsHealthIndicator:v1" in base_js assert "sessionStorage.getItem(cacheKey)" in base_js assert "sessionStorage.setItem(cacheKey" in base_js assert "const cacheTtlMs = 60000" in base_js assert "new AbortController()" in base_js assert "setTimeout(() => controller.abort(), 2500)" in base_js assert "setInterval(() => refresh(false), 60000)" in base_js assert "_HEALTH_INDICATOR_CACHE_LOCK" in observability_route assert "_HEALTH_INDICATOR_CACHE_TTL_SECONDS = 30" in observability_route assert "return jsonify(dict(cached_payload))" in observability_route def test_market_intel_disabled_page_stays_lightweight_and_action_oriented(): template_path = ROOT / "templates/market_intel/disabled.html" template = template_path.read_text(encoding="utf-8") assert template_path.stat().st_size < 40000 assert "市場情報入口" in template assert "比價覆核" in template assert "PChome 商品監控" in template assert "AI 觀測台" in template assert "system_version" not in template assert "V10." not in template assert "hotfix" not in template.lower() assert "Gitea" not in template assert "Codex" not in template assert "Claude" not in template assert "Runtime Status" not in template assert "Decision Flow" not in template assert "模組待啟用" not in template assert "停用中的試驗流程" not in template assert "data-market-intel-preview" not in template assert "/api/market_intel/" not in template assert "讀取候選預覽中" not in template def test_growth_workflow_pages_hide_raw_export_and_fallback_content(): pchome_crawler = (ROOT / "templates/pchome_crawler.html").read_text(encoding="utf-8") market_intel = (ROOT / "templates/market_intel/disabled.html").read_text(encoding="utf-8") settings = (ROOT / "templates/settings.html").read_text(encoding="utf-8") settings_js = (ROOT / "web/static/js/page-settings.js").read_text(encoding="utf-8") crawler_routes = (ROOT / "routes/crawler_management_routes.py").read_text(encoding="utf-8") navbar = (ROOT / "templates/components/_navbar.html").read_text(encoding="utf-8") shell = (ROOT / "templates/components/_ewoooc_shell.html").read_text(encoding="utf-8") dashboard_js = (ROOT / "web/static/js/page-dashboard-v2.js").read_text(encoding="utf-8") ai_recommend_js = (ROOT / "web/static/js/page-ai-recommend.js").read_text(encoding="utf-8") assert "PChome 商品監控" in pchome_crawler assert "商品清單" in pchome_crawler assert "下載賣場清單" in pchome_crawler assert "商品編號" in pchome_crawler assert "PChome 24h 官方賣場" in pchome_crawler assert "匯出 JSON" not in pchome_crawler assert "下載完整清單" not in pchome_crawler assert "exportJson" not in pchome_crawler assert "JSON.stringify(currentProducts" not in pchome_crawler assert "pchome_products.json" not in pchome_crawler assert "圖片URL" not in pchome_crawler assert "商品URL" not in pchome_crawler assert "PChome 爬蟲" not in pchome_crawler assert "爬蟲" not in pchome_crawler assert "資料狀態" in market_intel assert "PChome 商品監控" in market_intel assert "來源規格" in market_intel assert "AI 受控整理" in market_intel assert "DATA STATUS" not in market_intel assert "Adapter" not in market_intel assert "手動 Fetch" not in market_intel assert "API 不執行推版" not in market_intel assert "PChome 爬蟲" not in market_intel assert "商品監控中心" in settings assert "監控來源設定" in settings assert "escapeHtml(monitorText(info.name" in settings_js assert "每 ${scheduleHours} 小時更新" in settings_js assert "監控來源「" in crawler_routes assert "CRAWLER_CONFIG_PATH" in crawler_routes assert "'function', 'page_type', 'status', 'paused_date'" in crawler_routes assert '"message": f"爬蟲' not in crawler_routes assert "商品監控" in navbar assert "商品監控狀態" in shell assert "全站商品監控" in dashboard_js for source in (settings, navbar, shell, dashboard_js): assert "爬蟲" not in source assert "外部訊號已取得,但尚未整理成可直接判斷的摘要" in ai_recommend_js assert "商品判斷尚未整理成可執行摘要" in ai_recommend_js assert "
/decision', methods=['POST'])" in route_source
    assert "record_competitor_match_review" in route_source
    assert "clear_competitor_intel_cache()" in route_source
    assert "_extract_match_diagnostic_reasons" in route_source
    assert "妝效質地不同" in route_source
    assert "多款任選待確認" in route_source
    assert "MockRecord" not in route_source
    assert "{% for item in items %}" in dashboard
    assert "PChome 業績成長作戰台" in dashboard
    assert "先看業績,再決定調價、曝光與組合" in dashboard
    assert "growth-command-kpis" in dashboard
    assert "MOMO 比價後怎麼做" in dashboard
    assert "高業績商品作戰清單" in dashboard
    assert "業績 × MOMO 價格 × 下一步" in dashboard
    assert "growth.mapping_rate" in dashboard
    assert "growth.top_opportunities" in dashboard
    assert "比價監控總覽" in dashboard
    assert "決策支援覆蓋率" in dashboard
    assert "overview.decision_support_rate" in dashboard
    assert "overview.catalog_comparable_count" in dashboard
    assert "overview.catalog_identity_review_count" in dashboard
    assert "比價決策焦點" in dashboard
    assert "overview.match_rate" in dashboard
    assert "overview.stale_match_count" in dashboard
    assert "待刷新 {{ overview.stale_match_count" in dashboard
    assert "overview.top_picks" in dashboard
    assert "overview.top_momo_threats" in dashboard
    assert "overview.pending_priority" in dashboard
    assert "overview.review_queue" in dashboard
    assert "需單位價 {{ overview.unit_comparable_count" in dashboard
    assert "重算待覆核 {{ overview.rescore_accepted_count" in dashboard
    assert "review_status='catalog_identity_review'" in dashboard
    assert "身份採用待核" in dashboard
    assert "grid-template-columns: repeat(5, minmax(0, 1fr))" in dashboard_css
    assert ".growth-command-center" in dashboard_css
    assert ".growth-strategy-grid" in dashboard_css
    assert ".growth-opportunity-table" in dashboard_css
    assert "{% if review_total_is_estimated %}約 {% endif %}" in dashboard
    assert "filter='ai_picks'" in dashboard
    assert "filter='pchome_review'" in dashboard
    assert "review_status=option.key" in dashboard
    assert "需單位價" in dashboard
    assert "近門檻可救" in route_source
    assert "證據不足" in route_source
    assert "低信心舊候選" in route_source
    assert "'legacy_low_score'" in route_source
    assert "dashboard-review-segments" in dashboard
    assert "data-pchome-review-action" in dashboard
    assert "採用同款" in dashboard
    assert "否決候選" in dashboard
    assert "標記單位價" in dashboard
    assert "補搜尋" in dashboard
    assert "覆核建議:" in dashboard
    assert "review.catalog_review_guidance.action_hint" in dashboard
    assert 'data-review-action="needs_research"' in dashboard
    assert "人工閉環" in route_source
    assert "AI 挑品清單" in dashboard
    assert "比價覆核隊列" in dashboard
    assert "下一步" in dashboard
    assert "商品ID {{ product.i_code }}" in dashboard
    assert "開 MOMO 賣場" in dashboard
    assert "開 PChome 賣場" in dashboard
    assert "dashboard-ai-product-evidence" in dashboard
    assert "PChome 商品ID {{ ai_pchome_id }}" in dashboard
    assert "PChome 商品ID 待補" in dashboard
    assert "data-fallback-src" in dashboard
    assert "item.ai_pick.pchome_url" in dashboard
    assert ".dashboard-ai-product-evidence" in dashboard_css
    assert "is-ai-picks-wrap" in dashboard
    assert "dashboard-product-thumb-missing" in dashboard
    assert "dashboard-product-identity" in dashboard_css
    assert ".dashboard-table.is-ai-picks td:nth-child(6)" in dashboard_css
    assert "position: sticky;" in dashboard_css
    assert "min-width: 230px" in dashboard_css
    assert ".dashboard-table-wrap.is-ai-picks-wrap" in guard_css
    assert "min-width: 1660px !important" in guard_css
    assert "dashboard-ai-summary-grid" in dashboard
    assert "AI 建議" in dashboard
    assert "/api/export/excel/ai-picks" in dashboard
    assert "匯出 AI 挑品" in dashboard
    assert "item.ai_pick.reason" in dashboard
    assert "_summarize_ai_pick_selection(ai_pick_map)" in route_source
    assert "{{ ai_pick_list_limit }} 品" in dashboard
    assert "_load_ai_pick_selection(session, PRODUCT_PICK_LIST_LIMIT)" in route_source
    assert "WITH valid_competitor AS" in route_source
    assert "vc.competitor_product_url" in route_source
    assert "'pchome_id': pchome_id" in route_source
    assert "'pchome_url': pchome_url" in route_source
    assert "PRODUCT_PICK_LIST_LIMIT = 50" in route_source
    assert "ui='v2'" not in dashboard
    assert 'name="ui" value="v2"' not in dashboard
    assert "mockProducts" not in dashboard
    assert "假商品" not in dashboard


def test_ai_pick_export_uses_real_recommendation_data():
    export_source = (ROOT / "routes/export_routes.py").read_text(encoding="utf-8")

    assert "@export_bp.route('/api/export/excel/ai-picks')" in export_source
    assert "ai_price_recommendations ar" in export_source
    assert "competitor_prices cp" in export_source
    assert "LEFT JOIN products p ON p.i_code = ar.sku" in export_source
    assert "ROW_NUMBER() OVER" in export_source
    assert "LIMIT 50" in export_source
    assert "MOMO商品ID" in export_source
    assert "PChome商品ID" in export_source
    assert "AI建議理由" in export_source
    assert "pd.ExcelWriter" in export_source


def test_pchome_review_export_and_diagnostics_use_real_queue_data():
    export_source = (ROOT / "routes/export_routes.py").read_text(encoding="utf-8")
    route_source = (ROOT / "routes/dashboard_routes.py").read_text(encoding="utf-8")
    repository_source = (ROOT / "services/competitor_intel_repository.py").read_text(encoding="utf-8")
    dashboard = (ROOT / "templates/dashboard_v2.html").read_text(encoding="utf-8")
    dashboard_css = (ROOT / "web/static/css/page-dashboard-v2.css").read_text(encoding="utf-8")
    page_guard_css = (ROOT / "web/static/css/ewoooc-v3-page-guard.css").read_text(encoding="utf-8")

    assert "@export_bp.route('/api/export/excel/pchome-review')" in export_source
    assert "fetch_competitor_review_queue_page" in export_source
    assert "診斷原因" in export_source
    assert "_flatten_review_decision_envelope" in export_source
    assert "決策信封ID" in export_source
    assert "自動執行允許" in export_source
    assert "決策證據摘要" in export_source
    assert "原始診斷" in export_source
    assert "PChome比價覆核_" in export_source
    assert "MATCH_DIAGNOSTIC_REASON_LABELS" in repository_source
    assert "diagnostic_reasons" in repository_source
    assert "商品線不符" in repository_source
    assert "容量差異" in repository_source
    assert "妝效質地不同" in repository_source
    assert "工具功能不同" in repository_source
    assert "多款任選待確認" in repository_source
    assert "妝效質地不同" in route_source
    assert "_extract_match_diagnostic_reasons" in route_source
    assert "'detail': attempt.get('error_message')" not in route_source
    assert "需檢查 matcher diagnostics" not in route_source
    assert "匯出覆核" in dashboard
    assert "商品 / MOMO" in dashboard
    assert "PChome 待確認商品" in dashboard
    assert "開 PChome 待確認商品" in dashboard
    assert "確認採用這筆 PChome 商品為正式同款配對?" in dashboard
    assert "PChome 候選" not in dashboard
    assert "覆核判讀" in dashboard
    assert "dashboard-review-workbench-row" in dashboard
    assert "dashboard-review-candidate-title" in dashboard
    assert "dashboard-review-next-step" in dashboard
    assert "is-review-wrap" in dashboard
    assert "6 if current_filter == 'pchome_review'" in dashboard
    assert "review.review_bucket" not in dashboard
    assert "HITL" not in dashboard
    assert "stored_status" not in dashboard
    assert "stored_score" not in dashboard
    assert "matcher_rescore" not in dashboard
    assert 'title="{{ envelope.decision_id }}"' not in dashboard
    assert 'title="決策追蹤"' in dashboard
    assert "優先 {{ envelope.severity" in dashboard
    assert "證據完整" in dashboard
    assert "需人工" in dashboard
    assert "review.decision_envelope" in dashboard
    assert "dashboard-review-envelope" in dashboard
    assert "review.diagnostic_reasons" in dashboard
    assert "item.pchome_match_attempt.diagnostic_reasons" in dashboard
    assert "dashboard-review-reasons" in dashboard
    assert "dashboard-review-actions" in dashboard
    assert ".dashboard-review-workbench-row" in dashboard_css
    assert ".dashboard-review-candidate-title" in dashboard_css
    assert ".dashboard-review-next-step" in dashboard_css
    assert ".dashboard-table.is-review" in dashboard_css
    assert "min-width: 1540px" in dashboard_css
    assert ".dashboard-table-wrap.is-review-wrap" in page_guard_css
    assert "width: max(100%, 1540px) !important" in page_guard_css
    assert 'content: "商品/MOMO"' in page_guard_css
    assert 'content: "PChome待確認"' in page_guard_css
    assert 'content: "PChome候選"' not in page_guard_css
    assert ".dashboard-review-reasons" in dashboard_css
    assert ".dashboard-review-envelope" in dashboard_css
    assert ".dashboard-review-actions" in dashboard_css
    assert ".dashboard-review-action.is-research" in dashboard_css
    assert "grid-template-columns: repeat(auto-fit, minmax(128px, 1fr))" in dashboard_css


def test_ai_intelligence_uses_v2_shell_and_real_runtime_apis():
    template = (ROOT / "templates/ai_intelligence.html").read_text(encoding="utf-8")
    route_source = (ROOT / "routes/ai_routes.py").read_text(encoding="utf-8")

    assert "{% extends 'ewoooc_base.html' %}" in template
    assert "{% block ewooo_content %}" in template
    assert "ai-intel-hero" in template
    assert "ai-status-badge" in template
    assert "PChome 業績成長自動化作戰系統" in template
    assert "MOMO 外部價格參考" in template
    assert "今日重點總覽" in template
    assert "今日任務摘要" in template
    assert "growth-executive-strip" in template
    assert "growthExecTask" in template
    assert "renderGrowthExecutiveSummary" in template
    assert "商品處理進度" in template
    assert "外部價格來源" in template
    assert "nextActionTitle" in template
    assert "renderNextAction" in template
    assert "今天先做:補齊" in template
    assert "今天先做:檢查價格風險" in template
    assert "價格風險分佈" in template
    assert "備援資料檢查" in template
    assert "外部報價預檢" not in template
    assert "鎖定商品" in template
    assert "data-label=\"PChome\"" in template
    assert "data-action=\"backfill\"" in template
    assert "data-action=\"generate-picks\"" in template
    assert "data-action=\"trigger-analysis\"" in template
    assert "setActionBusy" in template
    assert "showToast" in template
    assert "readJsonResponse" in template
    assert "msg.textContent" in template
    assert "msg.innerHTML" not in template
    assert "focusPriceTable" in template
    assert "priceRiskBoard" in template
    assert "growth-ops-table" in template
    assert "資料可信度" in template
    assert "PChome 貴" in template
    assert "PChome 便宜" in template
    assert "data-label=\"資料可信度\"" in template
    assert "renderOpsCommandDashboard" in template
    assert "renderPriceRiskBoard" in template
    assert "需檢查價格" in template
    assert "留意價差" in template
    assert "PChome 便宜" in template
    assert "compSourceSummary" in template
    assert "'external_offers':" in template
    assert "'自動同步'" in template
    assert "price_basis_label" in template
    assert "商品總價" in template
    assert "/api/ai/pchome-growth/opportunities" in template
    assert "最近處理紀錄" in template
    assert "處理紀錄" in template
    assert "作戰建議紀錄" not in template
    assert "fetch('/api/ai/icaim/dashboard')" in template
    assert "fetch('/api/ai/product-picks/generate'" in template
    assert "fetch('/api/ai/pchome-match/backfill'" in template
    assert "JSON.stringify({ limit: 50 })" in template
    assert "僅顯示已確認同款的商品" in template
    assert "tagMap[t] || ['bg-light text-dark', t]" not in template
    assert "\"match_type_exact\":[" not in template
    assert "'match_type_exact':[" in template
    assert "'同款確認'" in template
    assert "mock" not in template.lower()
    assert "假商品" not in template

    assert "@ai_bp.route('/ai_intelligence')" in route_source
    assert "render_template('ai_intelligence.html', active_page='ai_intelligence')" in route_source
    assert "@ai_bp.route('/api/ai/icaim/dashboard')" in route_source
    assert "FROM external_offers eo" in route_source
    assert "自動同步資料層" in route_source
    assert "competitor_data_source_counts" in route_source
    assert "competitor_prices" in route_source
    assert "ai_price_recommendations" in route_source
    assert "_ICAIM_DASHBOARD_TTL_SECONDS" in route_source
    assert "_ICAIM_DB_STATEMENT_TIMEOUT_MS" in route_source
    assert "JOIN LATERAL" in route_source
    assert "DISTINCT ON (cp.sku)" in route_source
    assert "_get_cached_icaim_dashboard_payload(allow_stale=True)" in route_source


def test_price_comparison_page_is_action_oriented_and_plain_chinese():
    template = (ROOT / "templates/price_comparison.html").read_text(encoding="utf-8")
    route_source = (ROOT / "routes/price_comparison_routes.py").read_text(encoding="utf-8")

    assert "{% extends \"ewoooc_base.html\" %}" in template
    assert "PChome 商品比價決策台" in template
    assert "price-hero-kpis" in template
    assert "priceDecisionGrid" in template
    assert "檢查範圍" in template
    assert "比價流程" in template
    assert "price-workflow-strip" in template
    assert "price-result-summary-grid" in template
    assert "priceResultHeadline" in template
    assert "renderPriceDecisionCards" in template
    assert "renderPriceWorkflow" in template
    assert "今天先做:選擇要檢查的商品範圍" in template
    assert "資料準備狀態" in template
    assert "priceNextActionButton" in template
    assert "renderPriceCommandDashboard" in template
    assert "runPriceNextAction" in template
    assert "fetchTargetedMomoBtn" in template
    assert "自動找 MOMO 候選" in template
    assert "fetchTargetedMomoCandidates" in template
    assert "momoUnitCompareCandidates" in template
    assert "momoUnitCompareCount" in template
    assert "renderMomoUnitComparePanel" in template
    assert "自動單位價比較" in template
    assert "查看單位價" in template
    assert "sync_external_offers: true" in template
    assert "已同步 ${syncedCount} 筆到作戰清單" in template
    assert "renderMomoReviewPanel" in template
    assert "/api/price_comparison/fetch_momo_for_pchome" in template
    assert "MOMO 候選待確認" in template
    assert "待確認 ${momoReviewCandidates.length} 筆:先並排看兩家賣場" in template
    assert "price-review-compare" in template
    assert "price-review-store is-pchome" in template
    assert "price-review-store is-momo" in template
    assert "同時開兩家賣場" in template
    assert "確認 MOMO 單品/組合候選" in template
    assert "比價結果判讀" in template
    assert "需檢查價格" in template
    assert "可主推曝光" in template
    assert "價格接近" in template
    assert "檢查售價" in template
    assert "主推曝光" in template
    assert "PChome 貴" in template
    assert "PChome 便宜" in template
    assert "雙開賣場" in template
    assert "openComparisonStores" in template
    assert "data-pchome-url" in template
    assert "data-momo-url" in template
    assert "Excel 至少要有商品名稱與售價" in template
    assert "貼上 MOMO 商品、售價與賣場連結" in template
    assert "先選擇 MOMO 商品檔案。" in template
    assert "先貼上商品、售價與賣場連結。" in template
    assert "resetComparisonResult" in template
    assert "showToast" in template
    assert "text.textContent = message" in template
    assert "toast.innerHTML" not in template
    assert "格式說明" not in template
    assert "商品名稱,價格" not in template
    assert "欄位" not in template
    assert "系統會整理成可比價清單" not in template
    assert "請選擇檔案" not in template
    assert "請輸入商品資料" not in template
    assert "Step 1" not in template
    assert "Step 2" not in template
    assert "Step 3" not in template
    assert "開始比價" not in template
    assert "PChome vs MOMO 比價" not in template
    assert "爬取 PChome..." not in template
    assert "匹配 ${comparisonResult.matched_count}" not in template
    assert "@price_comparison_bp.route('/price_comparison')" in route_source
    assert "@price_comparison_bp.route('/api/price_comparison/fetch_momo_for_pchome', methods=['POST'])" in route_source
    assert "render_template('price_comparison.html', active_page='price_comparison')" in route_source


def test_utility_pages_keep_operator_copy_professional():
    ppt_history = (ROOT / "templates/admin/ppt_audit_history.html").read_text(encoding="utf-8")
    ppt_preview = (ROOT / "templates/admin/ppt_audit_preview.html").read_text(encoding="utf-8")
    auto_import = (ROOT / "templates/auto_import_index.html").read_text(encoding="utf-8")
    stockout_import = (ROOT / "templates/vendor_stockout_import_v2.html").read_text(encoding="utf-8")
    vendor_import_js = (ROOT / "web/static/js/page-vendor-import.js").read_text(encoding="utf-8")
    stockout_index = (ROOT / "templates/vendor_stockout_index_v2.html").read_text(encoding="utf-8")
    stockout_history = (ROOT / "templates/vendor_stockout_history_v2.html").read_text(encoding="utf-8")
    stockout_list = (ROOT / "templates/vendor_stockout_list_v2.html").read_text(encoding="utf-8")
    stockout_send_email = (ROOT / "templates/vendor_stockout_send_email_v2.html").read_text(encoding="utf-8")
    stockout_vendor_management = (ROOT / "templates/vendor_stockout_vendor_management_v2.html").read_text(encoding="utf-8")
    quality_trend = (ROOT / "templates/admin/quality_trend.html").read_text(encoding="utf-8")
    rag_queries = (ROOT / "templates/admin/rag_queries.html").read_text(encoding="utf-8")
    ai_calls = (ROOT / "templates/admin/ai_calls_dashboard.html").read_text(encoding="utf-8")
    observability_labels = (ROOT / "templates/admin/_observability_labels.html").read_text(encoding="utf-8")
    host_health = (ROOT / "templates/admin/host_health.html").read_text(encoding="utf-8")
    cicd_dashboard = (ROOT / "templates/cicd_dashboard.html").read_text(encoding="utf-8")
    logs_page = (ROOT / "templates/logs.html").read_text(encoding="utf-8")
    logs_js = (ROOT / "web/static/js/page-logs.js").read_text(encoding="utf-8")
    navbar = (ROOT / "templates/components/_navbar.html").read_text(encoding="utf-8")
    observability_js = (ROOT / "web/static/js/observability-charts.js").read_text(encoding="utf-8")
    budget = (ROOT / "templates/admin/budget.html").read_text(encoding="utf-8")
    agent_orchestration = (ROOT / "templates/admin/agent_orchestration.html").read_text(encoding="utf-8")
    ai_automation = (ROOT / "templates/ai_automation_smoke.html").read_text(encoding="utf-8")
    code_review = (ROOT / "templates/code_review.html").read_text(encoding="utf-8")
    login = (ROOT / "templates/login.html").read_text(encoding="utf-8")
    combined = "\n".join([
        ppt_history,
        ppt_preview,
        auto_import,
        stockout_import,
        vendor_import_js,
        stockout_index,
        stockout_history,
        stockout_list,
        stockout_send_email,
        stockout_vendor_management,
        quality_trend,
        rag_queries,
        ai_calls,
        observability_labels,
        host_health,
        cicd_dashboard,
        logs_page,
        logs_js,
        navbar,
        observability_js,
        budget,
        agent_orchestration,
        ai_automation,
        code_review,
        login,
    ])

    assert "簡報線上預覽" in ppt_preview
    assert "下載簡報檔" in ppt_history
    assert "送出後更新日報、成長分析與今日作戰清單" in auto_import
    assert "等待系統更新任務狀態;若重複停在異常" in auto_import
    assert "return raw ||" not in auto_import
    assert "供貨風險匯入" in stockout_import
    assert "缺少必要內容時,會先停止匯入" in stockout_import
    assert "缺貨資料" not in stockout_import
    assert "STOCKOUT IMPORT" not in stockout_import
    assert "先選擇供應商缺貨 Excel 檔。" in vendor_import_js
    assert "供貨風險清單" in stockout_list
    assert "缺貨處理清單" in stockout_list
    assert "缺貨資料表" not in stockout_list
    assert "補貨通知紀錄" in stockout_send_email
    assert "供貨風險" in stockout_send_email
    assert "Vendor Stockout" not in stockout_send_email
    assert "供應商窗口" in stockout_vendor_management
    assert "維護正確窗口" in stockout_vendor_management
    assert "Vendor Stockout" not in stockout_vendor_management
    assert "使用情境反饋分佈" in quality_trend
    assert "最低使用情境平均分" in quality_trend
    assert "{{ caller }}" not in quality_trend
    assert "{{ rc.caller }}" not in quality_trend
    assert "{{ rec.caller }}" not in quality_trend
    assert "全部使用情境" in rag_queries
    assert "各使用情境知識表現" in rag_queries
    assert "僅看已省下模型呼叫" in rag_queries
    assert "top_k" not in rag_queries
    assert "{{ q.caller }}" not in rag_queries
    assert "{{ c.caller }}" not in rag_queries
    assert "使用情境" in ai_calls
    assert "全部使用情境" in ai_calls
    assert "情境 × 知識命中矩陣" in ai_calls
    assert "服務更新監控" in cicd_dashboard
    assert "正式服務更新與可用性監控" in cicd_dashboard
    assert "測試站狀態" in cicd_dashboard
    assert "正式站狀態" in cicd_dashboard
    assert "監控圖表" in cicd_dashboard
    assert "自動化流程" in cicd_dashboard
    assert "issue.error_log" not in cicd_dashboard
    assert "UAT 狀態" not in cicd_dashboard
    assert "PROD 狀態" not in cicd_dashboard
    assert ">Grafana<" not in cicd_dashboard
    assert ">n8n<" not in cicd_dashboard
    assert "系統事件紀錄" in logs_page
    assert "事件數" in logs_page
    assert "需處理" in logs_page
    assert "需追蹤" in logs_page
    assert "下載事件摘要" in logs_page
    assert "系統事件" in navbar
    assert "toEventSummary" in logs_js
    assert "buildEventSummaryText" in logs_js
    assert "rawLogs" not in logs_js
    assert "momo_system_logs_" not in logs_js
    assert "系統日誌" not in logs_page
    assert "下載日誌" not in logs_page
    assert "系統日誌" not in navbar
    assert "供貨風險" in stockout_index
    assert "無服務資料 / 未連線" in host_health
    assert "部署檢查已排入背景處理" in observability_js
    assert "Top 5 成本使用情境" in budget
    assert "工具協作 × 使用情境" in agent_orchestration
    assert "下載檢查紀錄" in ai_automation
    assert "健康檢查服務" in ai_automation
    assert "上線證據" in code_review
    assert "資料服務" in login
    system_settings = (ROOT / "templates/system_settings.html").read_text(encoding="utf-8")
    assert "系統操作暫時沒有完成,請稍後重試" in system_settings
    assert "return raw;" not in system_settings

    forbidden = [
        "工作視窗",
        "Codex",
        "Claude",
        "推到 Gitea",
        "本輪已完成",
        "剛剛修正",
        "後續 session",
        "JSONL",
        "健康檢查 API",
        "JSON.stringify(item.details",
        "PostgreSQL",
        "CSRF 防護",
        "Session 2h",
        "燒錢呼叫端",
        "{{ m.caller }}",
        "{{ m.server }}",
        "PPT Online Preview",
        "Preview unavailable",
        "原始 PPTX",
        "原始:",
        "無模型資料",
        "請選擇 Excel 檔案",
        "Vendor Stockout",
        "VENDOR STOCKOUT",
        "全部呼叫端",
        "呼叫端 ×",
        "{{ c.caller }}",
        "{{ q.caller }}",
        "{{ caller }}",
        "NIM Elephant",
        "OpenRouter",
        "GitLab 部署紀錄",
        "${p.ref} @ ${p.sha}",
        "error_log.substring",
        "${escapeHtml(env.error)}",
        "系統會去重",
        "系統會拒絕",
        "管線 ID",
        "Commit:",
        "變更檔案:",
        "查看系統日誌",
        "下載日誌",
        "rawLogs",
        "momo_system_logs_",
        "could not locate runnable browser",
        "daily_sales_snapshot",
        "realtime_sales_monthly",
        "snapshot_date",
        "psycopg2",
    ]
    for marker in forbidden:
        assert marker not in combined


def test_ai_history_uses_v2_shell_and_real_history_apis():
    template = (ROOT / "templates/ai_history.html").read_text(encoding="utf-8")
    route_source = (ROOT / "routes/ai_routes.py").read_text(encoding="utf-8")

    assert "{% extends 'ewoooc_base.html' %}" in template
    assert "{% block ewooo_content %}" in template
    assert "{% block extra_css %}" in template
    assert "ai-history-hero" in template
    assert "ai-history-panel" in template
    assert "fetch('/api/ai/statistics?days=30')" in template
    assert "fetch(`/api/ai/history?${params}`)" in template
    assert "fetch(`/api/ai/history/${id}`" in template
    assert "fetch('/api/ai/history/batch'" in template
    assert "mock" not in template.lower()
    assert "假商品" not in template

    assert "@ai_bp.route('/ai_history')" in route_source
    assert "render_template('ai_history.html', active_page='ai_history')" in route_source
    assert "@ai_bp.route('/api/ai/history')" in route_source
    assert "ai_history_service.get_history_list" in route_source
    assert "ai_history_service.get_statistics" in route_source


def test_ai_recommend_uses_v2_shell_and_runtime_category_data():
    template = (ROOT / "templates/ai_recommend.html").read_text(encoding="utf-8")
    page_js = (ROOT / "web/static/js/page-ai-recommend.js").read_text(encoding="utf-8")
    route_source = (ROOT / "routes/ai_routes.py").read_text(encoding="utf-8")

    assert "{% extends 'ewoooc_base.html' %}" in template
    assert "{% block ewooo_content %}" in template
    assert "{% block extra_css %}" in template
    assert "ai-recommend-hero" in template
    assert "ai-recommend-page" in template
    assert "{% for category in product_categories[:4] %}" in template
    assert "quickWebSearch({{ category|tojson }})" in template
    assert "setProduct({{ category|tojson }})" in template
    assert "quickWebSearch('保濕面膜')" not in template
    assert "fetch('/api/ai/generate_copy'" in page_js
    assert "fetch('/api/ai/web_search'" in page_js
    assert "fetch('/api/ai/product_insights'" in page_js
    assert "fetch('/api/ai/gemini_usage?days=30')" in page_js
    assert "renderBestsellerCard" in page_js
    assert "ar-product-card__img" in page_js
    assert "待補圖片" in page_js
    assert "商品 ID" in page_js
    assert "開賣場" in page_js
    assert "product_id: el.dataset.productId" in page_js
    assert "Object.assign(window" in page_js
    assert "setProductFromCard" in page_js
    assert "商品 ID、圖片與賣場連結可一眼確認" in template
    assert 'id="platformMomo" value="momo">' in template
    assert "mock" not in template.lower()
    assert "假商品" not in template
    assert "PChome 銷售建議" in template
    assert "銷售動作生成" in template
    assert "建議目的" in template
    assert "處理順序" in template
    assert "建議引擎" in template
    assert "備援守門" in template
    assert "ar-engine-settings" in template
    assert "整理訊號" in page_js
    assert "商品判斷暫時不可用" in page_js

    forbidden_visible_text = [
        "🖥️ Ollama (本地)",
        "☁️ Gemini (雲端)",
        "Ollama 主路徑",
        "Gemini 備援",
        "Gemini 備援(系統自動,不可手動選)",
        "disabled>☁️ Gemini 備援",
        "AI 模型主路徑",
        "AI 路徑",
        "分析模型",
        "Web Search 功能",
        "渲染 Web Search",
        "整合 Web Search",
        "Token:",
        "權杖:",
        "費用:",
        "費用:",
        "生成失敗:",
        "生成失敗:",
        "發生錯誤:",
        "發生錯誤:",
        "搜尋失敗:",
        "搜尋失敗:",
        "分析失敗:",
        "分析失敗:",
    ]
    combined = template + "\n" + page_js
    for marker in forbidden_visible_text:
        assert marker not in combined

    assert "@ai_bp.route('/ai_recommend')" in route_source
    assert "render_template('ai_recommend.html'" in route_source
    assert "active_page='ai_recommend'" in route_source
    assert "product_categories=product_categories" in route_source
    assert "@ai_bp.route('/api/ai/generate_copy'" in route_source
    assert "@ai_bp.route('/api/ai/web_search'" in route_source
    assert "@ai_bp.route('/api/ai/product_insights'" in route_source
    assert "'product_id': p.product_id" in (ROOT / "services/pchome_crawler.py").read_text(encoding="utf-8")
    assert "'product_id': p.product_id" in (ROOT / "services/momo_crawler.py").read_text(encoding="utf-8")


def test_monthly_summary_analysis_uses_v2_shell_and_real_monthly_api():
    template = (ROOT / "templates/monthly_summary_analysis.html").read_text(encoding="utf-8")
    route_source = (ROOT / "routes/monthly_routes.py").read_text(encoding="utf-8")
    script = (ROOT / "web/static/js/page-monthly-summary.js").read_text(encoding="utf-8")

    assert "{% extends 'ewoooc_base.html' %}" in template
    assert "{% block ewooo_content %}" in template
    assert "{% block extra_css %}" in template
    assert "{% block extra_js %}" in template
    assert "components/_navbar.html" not in template
    assert "" not in template
    assert "monthly-analysis-hero" in template
    assert "monthly-analysis-page" in template
    assert "/api/monthly_summary_data" in template
    assert "/api/monthly_summary_trend" in route_source
    assert "/api/monthly_summary_trend" in script
    assert "area_name=${encodeURIComponent(area)}&limit=1" not in script
    assert "monthly_summary_analysis" in route_source
    assert "active_page='monthly'" in route_source
    assert "MonthlySummaryAnalysis" in route_source
    assert "mock" not in template.lower()
    assert "假商品" not in template


def test_dashboard_v2_restores_real_price_history_chart():
    route_source = (ROOT / "routes/api_routes.py").read_text(encoding="utf-8")
    dashboard = (ROOT / "templates/dashboard_v2.html").read_text(encoding="utf-8")
    page_js = (ROOT / "web/static/js/page-dashboard-v2.js").read_text(encoding="utf-8")

    assert "@api_bp.route('/api/history/')" in route_source
    assert "PRICE_HISTORY_RANGES" in route_source
    assert "'week': {'days': 7" in route_source
    assert "'month': {'days': 30" in route_source
    assert "'quarter': {'days': 90" in route_source
    assert "'year': {'days': 365" in route_source
    assert "session.query(PriceRecord)" in route_source
    assert "PriceRecord.product_id == product.id" in route_source
    assert "js/page-dashboard-v2.js" in dashboard
    assert "https://cdn.jsdelivr.net/npm/chart.js" in page_js
    assert 'id="historyModal"' in dashboard
    assert 'id="priceChart"' in dashboard
    assert "data-product-id=\"{{ product.id }}\"" in dashboard
    assert "data-history-trigger" in dashboard
    assert "document.querySelectorAll('[data-history-trigger]')" in page_js
    assert "event.stopPropagation();" in page_js
    assert "showHistory(button.dataset.productId, button.dataset.productName);" in page_js
    assert "data-history-range=\"week\"" in dashboard
    assert "data-history-range=\"month\"" in dashboard
    assert "data-history-range=\"quarter\"" in dashboard
    assert "data-history-range=\"year\"" in dashboard
    assert "fetch(`/api/history/${productId}?range=${activeHistoryRange}&format=v2`)" in page_js
    assert "priceChartInstance = new Chart" in page_js
    assert "目前沒有可顯示的歷史價格紀錄" in page_js


def test_dashboard_v2_shows_pchome_competitor_pricing_and_links():
    route_source = (ROOT / "routes/dashboard_routes.py").read_text(encoding="utf-8")
    api_source = (ROOT / "routes/api_routes.py").read_text(encoding="utf-8")
    feeder_source = (ROOT / "services/competitor_price_feeder.py").read_text(encoding="utf-8")
    scheduler_source = (ROOT / "scheduler.py").read_text(encoding="utf-8")
    dashboard = (ROOT / "templates/dashboard_v2.html").read_text(encoding="utf-8")
    page_js = (ROOT / "web/static/js/page-dashboard-v2.js").read_text(encoding="utf-8")
    migration = (ROOT / "migrations/022_competitor_price_history.sql").read_text(encoding="utf-8")

    assert "_load_pchome_competitor_map(" in route_source
    assert "FROM competitor_prices" in route_source
    assert "competitor_product_id" in route_source
    assert "https://24h.pchome.com.tw/prod/" in route_source
    assert "_build_competitor_decision(" in route_source
    assert "PChome 價格優勢" in route_source
    assert "MOMO 低價壓力" in route_source
    assert "item['pchome_competitor']" in route_source
    assert "item['competitor_decision']" in route_source

    assert "MOMO 價格" in dashboard
    assert "PChome 價格" in dashboard
    assert "競價判讀" in dashboard
    assert "MOMO {{ product.i_code }}" in dashboard
    assert "PChome {{ competitor.product_id }}" in dashboard
    assert "competitor.product_url" in dashboard
    assert "dashboard-competition-badge" in dashboard
    assert "decision.summary" in dashboard
    assert "PChome {{ match_status.label" in dashboard
    assert "候選:{{ item.pchome_match_attempt.best_competitor_product_name }}" in dashboard
    assert "候選價,需單位換算" in dashboard
    assert "尚未進入 PChome 補抓" in dashboard
    assert '待比對' not in dashboard
    assert "PChome 比價補強產線" in dashboard
    assert "待比對補抓產線" not in dashboard
    assert "_load_pchome_match_attempt_map" in route_source
    assert "低信心待補強" in route_source
    assert "未找到可信同款" in route_source
    assert "規格不符已排除" in route_source
    assert "dashboard-review-reasons" in dashboard
    assert "series.pchome" in page_js
    assert "label: 'PChome'" in page_js
    assert "含 PChome 歷史快照" in page_js

    assert "competitor_price_history" in migration
    assert "momo_price" in migration
    assert "competitor_product_id" in migration
    assert "CREATE INDEX IF NOT EXISTS idx_comp_price_history_sku_source_time" in migration

    assert "_ensure_competitor_price_history_table" in feeder_source
    assert "INSERT INTO competitor_price_history" in feeder_source
    assert "history_written" in feeder_source
    assert "momo_price" in feeder_source
    assert "competitor_price_history" in api_source
    assert "'series': {" in api_source
    assert "'pchome': competitor_data" in api_source
    assert "history_written" in scheduler_source


def test_ai_product_pick_agent_uses_real_competitor_data_and_dashboard_action():
    feeder_source = (ROOT / "services/competitor_price_feeder.py").read_text(encoding="utf-8")
    agent_source = (ROOT / "services/ai_product_pick_agent.py").read_text(encoding="utf-8")
    route_source = (ROOT / "routes/ai_routes.py").read_text(encoding="utf-8")
    scheduler_source = (ROOT / "scheduler.py").read_text(encoding="utf-8")
    template = (ROOT / "templates/ai_intelligence.html").read_text(encoding="utf-8")

    assert "MIN_MATCH_SCORE  = 0.76" in feeder_source
    assert "REPLACE_DIFFERENT_PRODUCT_SCORE" in feeder_source
    assert "marketplace_product_matcher" in feeder_source
    assert "MAX_SEARCH_TERMS" in feeder_source
    assert "_build_search_keywords" in feeder_source
    assert "_search_pchome_candidates" in feeder_source
    assert "page_cap = max(1, int(max_pages or SEARCH_MAX_PAGES))" in feeder_source
    assert "search_limit = SEARCH_LIMIT * page_cap" in feeder_source
    assert "bounded_search=True" in feeder_source
    assert "crawler.search_products(keyword, limit=search_limit, max_pages=page_cap)" in feeder_source
    assert "_fetch_unmatched_priority_skus" in feeder_source
    assert "_fetch_expired_identity_skus" in feeder_source
    assert "run_expired_identity_refresh" in feeder_source
    assert "fetch_product_details(requested_ids" in feeder_source
    assert "run_unmatched_priority" in feeder_source
    assert "PCHOME_BACKFILL_EXPIRED_REFRESH_LIMIT" in scheduler_source
    assert "run_expired_identity_refresh(limit=expired_refresh_limit)" in scheduler_source

    assert "generate_product_pick_list" in agent_source
    assert "clear_dashboard_cache()" in route_source
    assert "get_full_dashboard_data()" in route_source
    assert "dashboard_cache_warmed" in route_source
    assert "competitor_prices" in agent_source
    assert "competitor_price_history" in agent_source
    assert "daily_sales_snapshot" in agent_source
    assert "ai_price_recommendations" in agent_source
    assert "'product_pick'" in agent_source
    assert "PChomeProductPickAgent" in agent_source
    assert "PChome 價格優勢" in agent_source
    assert "_daily_sales_columns" in agent_source
    assert '"總業績"' in agent_source
    assert '"毛利"' in agent_source
    assert '"總成本"' in agent_source
    assert "evidence_quality" in agent_source
    assert "opportunity_score" in agent_source
    assert "margin_rate" in agent_source
    assert "missing_evidence" in agent_source
    assert "confidence_band" in agent_source
    assert "_supersede_old_picks" in agent_source
    assert "status = 'superseded'" in agent_source
    assert "{date_col}::date" in agent_source
    assert "conn.rollback()" in agent_source

    assert "@ai_bp.route('/api/ai/product-picks/generate', methods=['POST'])" in route_source
    assert "generate_product_pick_list(engine" in route_source
    assert "@ai_bp.route('/api/ai/pchome-match/backfill', methods=['POST'])" in route_source
    assert "@ai_bp.route('/api/ai/pchome-growth/backfill-momo-candidates', methods=['POST'])" in route_source
    assert "@ai_bp.route('/api/ai/pchome-match/refresh-stale', methods=['POST'])" in route_source
    assert "@ai_bp.route('/api/ai/pchome-match/recover-stale', methods=['POST'])" in route_source
    assert 'PCHOME_STALE_RECOVERY_ENABLED' in route_source
    assert "@ai_bp.route('/api/ai/pchome-match/backfill/status', methods=['GET'])" in route_source
    assert "_build_pchome_backfill_coverage_payload" in route_source
    assert "_build_pchome_revalidation_preview_payload" in route_source
    assert "_build_pchome_stale_recovery_preview_payload" in route_source
    assert "fetch_competitor_coverage" in route_source
    assert "'catalog_variant_review_count': int(coverage.get('catalog_variant_review_count') or 0)" in route_source
    assert "'catalog_unit_review_count': int(coverage.get('catalog_unit_review_count') or 0)" in route_source
    assert "'catalog_identity_review_count': int(coverage.get('catalog_identity_review_count') or 0)" in route_source
    assert "'catalog_review_plan': coverage.get('catalog_review_plan') or {}" in route_source
    assert "preview_retryable_candidate_revalidation" in route_source
    assert "preview_expired_identity_recovery" in route_source
    assert "revalidation_preview" in route_source
    assert "stale_recovery_preview" in route_source
    assert "_build_pchome_operation_backlog" in route_source
    assert "'catalog_variant_review': {" in route_source
    assert "'catalog_unit_review': {" in route_source
    assert "'catalog_identity_review': {" in route_source
    assert "_pick_pchome_recommended_next_action" in route_source
    assert "'operation_backlog': operation_backlog" in route_source
    assert "'recommended_next_action': _pick_pchome_recommended_next_action(operation_backlog)" in route_source
    assert "status['coverage'] = _build_pchome_backfill_coverage_payload()" in route_source
    assert "run_unmatched_priority(limit=unmatched_limit)" in route_source
    assert "stale_refresh_limit = max(5, min(40, max(5, limit // 3)))" in route_source
    assert "stale_refresh_result = feeder.run_expired_identity_refresh(limit=stale_refresh_limit)" in route_source
    assert "stale_identity_refresh" in route_source
    assert "run_expired_identity_refresh(limit=limit)" in route_source
    assert "run_expired_identity_search_recovery(limit=limit)" in route_source
    assert "stage='refreshing_stale'" in route_source
    assert "stage='recovering_stale'" in route_source
    assert "run_retryable_candidate_revalidation" in route_source
    assert "generate_product_pick_list(engine, limit=50)" in route_source
    assert "start_pchome_backfill_run" in route_source
    assert "finish_pchome_backfill_run" in route_source
    assert "payload.get('limit', 50)" in route_source
    assert "JSON.stringify({ limit: 50 })" in template
    assert "完成後會重算 AI 挑品清單" in route_source
    assert "match_rate" in route_source
    assert "decision_ready_rate" in route_source
    assert "product_pick_count" in route_source
    dashboard_route_source = (ROOT / "routes/dashboard_routes.py").read_text(encoding="utf-8")
    dashboard_template = (ROOT / "templates/dashboard_v2.html").read_text(encoding="utf-8")
    assert "model_footprint" in dashboard_route_source
    assert "_ai_pick_evidence_fields" in dashboard_route_source
    assert "avg_evidence_quality" in dashboard_route_source
    assert "top_missing_evidence" in dashboard_route_source
    assert "證據 {{ item.ai_pick.evidence_quality" in dashboard_template
    assert "dashboard-ai-evidence-chip" in dashboard_template
    assert "挑品數" in dashboard_template
    assert "平均信心" in dashboard_template
    assert "證據完整度" in dashboard_template
    assert "平均價差" in dashboard_template
    assert "最大價差" in dashboard_template
    assert "待補證據" in dashboard_template
    assert "PICK COUNT" not in dashboard_template
    assert "AVG CONFIDENCE" not in dashboard_template
    assert "EVIDENCE GAP" not in dashboard_template
    assert "AVG GAP" not in dashboard_template
    assert "BEST GAP" not in dashboard_template

    scheduler_source = (ROOT / "scheduler.py").read_text(encoding="utf-8")
    run_scheduler_source = (ROOT / "run_scheduler.py").read_text(encoding="utf-8")
    agent_actions_source = (ROOT / "services/agent_actions.py").read_text(encoding="utf-8")
    assert "def run_pchome_match_backfill_task" in scheduler_source
    assert "def run_pchome_growth_momo_backfill_task" in scheduler_source
    assert "_save_stats('pchome_match_backfill'" in scheduler_source
    assert "_save_stats('pchome_growth_momo_backfill'" in scheduler_source
    assert "retryable_candidate_revalidation_total" in scheduler_source
    assert "run_pchome_match_backfill_task" in run_scheduler_source
    assert "run_pchome_growth_momo_backfill_task" in run_scheduler_source
    assert "每日 10:30:pchome_match_backfill" in run_scheduler_source
    assert "每日 10:45:pchome_growth_momo_backfill" in run_scheduler_source
    assert '"run_pchome_match_backfill_task"' in agent_actions_source
    assert '"run_pchome_growth_momo_backfill_task"' in agent_actions_source

    assert "產生今日清單" in template
    assert "補齊比價資料" in template
    assert "ai-intel-legacy-status" in template
    assert "generatePickList" in template
    assert "backfillPchomeMatches" in template
    assert "/api/ai/product-picks/generate" in template
    assert "/api/ai/pchome-match/backfill" in template
    assert "/api/ai/pchome-match/refresh-stale" in dashboard_template
    assert "/api/ai/pchome-match/recover-stale" not in dashboard_template
    assert "/api/ai/pchome-match/backfill/status" in dashboard_template
    assert "PChome 比價補強" in dashboard_template
    assert "data-pchome-growth-backfill-trigger" in dashboard_template
    assert "data-pchome-growth-backfill-status" in dashboard_template
    assert "每日 10:45 自動補對應" in dashboard_template
    assert "PCHOME MATCH BACKFILL" not in dashboard_template
    assert ">ACTIVE<" not in dashboard_template
    assert "目前 ACTIVE 商品" not in dashboard_template
    assert "data-pchome-backfill-trigger" in dashboard_template
    assert "data-pchome-refresh-stale-trigger" in dashboard_template
    assert "data-pchome-recover-stale-trigger" not in dashboard_template
    assert "刷新過期 120 筆" in dashboard_template
    assert "救援過期 40 筆" not in dashboard_template
    dashboard_js = (ROOT / "web/static/js/page-dashboard-v2.js").read_text(encoding="utf-8")
    assert "/api/ai/pchome-growth/backfill-momo-candidates" in dashboard_js
    assert "backfillPchomeGrowthMomoCandidates" in dashboard_js
    assert "loadPchomeBackfillStatus" in dashboard_js
    assert "window.backfillPchomeMatches" in dashboard_js
    assert "window.refreshStalePchomeMatches" in dashboard_js
    assert "window.recoverStalePchomeMatches" not in dashboard_js
    assert "formatBackfillCoverageSummary" in dashboard_js
    assert "formatBackfillStageSummary" in dashboard_js
    assert "補強 60 筆" in dashboard_js
    assert "啟動 PChome 比價補強" in dashboard_js
    assert "刷新', result.stale_identity_refresh" in dashboard_js
    assert "formatBackfillLimitedCount" in dashboard_js
    assert "status.coverage" in dashboard_js
    assert "coverage.recommended_next_action" in dashboard_js
    assert "建議 ${recommended.label}" in dashboard_js
    assert "決策支援 ${formatBackfillRate(coverage.decision_support_rate || coverage.decision_ready_rate)}" in dashboard_js
    assert "精準可用 ${formatBackfillRate(coverage.decision_ready_rate)}" in dashboard_js
    assert "型錄可比 ${formatBackfillCount(coverage.catalog_comparable_count)}" in dashboard_js
    assert "待刷新 ${formatBackfillCount(coverage.stale_matches)}" in dashboard_js
    assert "待補抓 ${formatBackfillCount(coverage.pending)}" in dashboard_js
    assert "可重評 ${formatBackfillLimitedCount(preview.candidate_count" in dashboard_js
    assert "可救援 ${formatBackfillLimitedCount(staleRecovery.candidate_count" in dashboard_js
    assert "'product_pick':['bg-success'" in template
    assert "kpiMatchRate" in template


def test_edm_dashboard_v2_is_production_default_and_uses_real_campaign_data():
    route_source = (ROOT / "routes/edm_routes.py").read_text(encoding="utf-8")
    template = (ROOT / "templates/edm_dashboard_v2.html").read_text(encoding="utf-8")
    page_js = (ROOT / "web/static/js/page-edm-v2.js").read_text(encoding="utf-8")

    assert "request.args.get('ui') == 'legacy'" not in route_source
    assert route_source.count("template_name = 'edm_dashboard_v2.html'") == 5
    assert "{% for slot, stats in slot_stats.items() %}" in template
    assert "{% for item in items %}" in template
    assert "scheduler_stats.get(task_key, [])" in template
    assert "@api_bp.route('/api/history/i-code/')" in (ROOT / "routes/api_routes.py").read_text(encoding="utf-8")
    assert "data-campaign-filter=\"new\"" in template
    assert "data-campaign-filter=\"up\"" in template
    assert "data-campaign-filter=\"down\"" in template
    assert "data-campaign-filter=\"delisted\"" in template
    assert "data-campaign-history-trigger" in template
    assert "showCampaignHistory(button.dataset.iCode, button.dataset.productName)" in page_js
    assert "fetch(`/api/history/i-code/${encodeURIComponent(iCode)}?range=${activeCampaignHistoryRange}`)" in page_js
    assert "data-campaign-history-range=\"week\"" in template
    assert "data-campaign-history-range=\"month\"" in template
    assert "data-campaign-history-range=\"quarter\"" in template
    assert "data-campaign-history-range=\"year\"" in template
    assert "applyCampaignFilter(card, button.dataset.campaignFilter)" in page_js
    assert "?ui=v2" not in template
    assert "ui='v2'" not in template
    assert "mock" not in template.lower()
    assert "假商品" not in template


def test_vendor_stockout_v2_is_production_default_and_uses_real_vendor_data():
    route_source = (ROOT / "routes/vendor_routes.py").read_text(encoding="utf-8")
    service_source = (ROOT / "services/vendor_stockout_query_service.py").read_text(encoding="utf-8")
    template = (ROOT / "templates/vendor_stockout_index_v2.html").read_text(encoding="utf-8")

    assert "vendor_stockout_index_v2.html" in route_source
    assert "request.args.get('ui') == 'legacy'" not in route_source
    assert not (ROOT / "web/templates/vendor_stockout_index_v2.html").exists()
    assert "get_vendor_dashboard_stats(vendor_db)" in route_source
    assert "def get_vendor_dashboard_stats(vendor_db)" in service_source
    assert "session.query(VendorStockout).count()" in service_source
    assert "EmailSendLog" in service_source
    assert "stats.pending_stockouts" in template
    assert "stats.email_success_rate" in template
    assert "ui='v2'" not in template
    assert "mock" not in template.lower()
    assert "假" not in template


def test_vendor_stockout_list_v2_is_production_default_and_uses_real_stockout_rows():
    route_source = (ROOT / "routes/vendor_routes.py").read_text(encoding="utf-8")
    service_source = (ROOT / "services/vendor_stockout_query_service.py").read_text(encoding="utf-8")
    template = (ROOT / "templates/vendor_stockout_list_v2.html").read_text(encoding="utf-8")

    assert "vendor_stockout_list_v2.html" in route_source
    assert "get_vendor_stockout_list_context(" in route_source
    assert "request.args.get('page', 1, type=int)" in route_source
    assert "def get_vendor_stockout_list_context(" in service_source
    assert "_apply_stockout_filters(" in service_source
    assert "session.query(VendorStockout)" in service_source
    assert "VendorStockout.status == 'pending'" in service_source
    assert "VendorStockout.batch_id == batch_id" in service_source
    assert "from flask" not in service_source
    assert "{% for record in records %}" in template
    assert "{% for batch in batches %}" in template
    assert "stats.pending" in template
    assert "record.product_code" in template
    assert "record.product_name" in template
    assert "record.vendor_name" in template
    assert "ui='v2'" not in template
    assert "mock" not in template.lower()
    assert "假" not in template


def test_vendor_stockout_api_queries_are_extracted_to_service():
    route_source = (ROOT / "routes/vendor_routes.py").read_text(encoding="utf-8")
    service_source = (ROOT / "services/vendor_stockout_query_service.py").read_text(encoding="utf-8")

    api_section = route_source.split("def api_get_stockout_list():", 1)[1].split("@vendor_bp.route('/api/stockout/'", 1)[0]

    assert "get_stockout_api_list_payload(" in api_section
    assert "get_stockout_batches_payload(vendor_db)" in api_section
    assert "session.query(VendorStockout)" not in api_section
    assert "def get_stockout_api_list_payload(" in service_source
    assert "def get_stockout_batches_payload(vendor_db)" in service_source
    assert "'batch_number': record.batch_id" in service_source
    assert "'send_status': record.status or 'pending'" in service_source
    assert "'latest_date': batch.latest_date.isoformat()" in service_source
    assert "from flask" not in service_source


def test_vendor_management_queries_are_extracted_to_service():
    route_source = (ROOT / "routes/vendor_routes.py").read_text(encoding="utf-8")
    service_source = (ROOT / "services/vendor_stockout_query_service.py").read_text(encoding="utf-8")

    api_section = route_source.split("def api_get_vendor_list():", 1)[1].split("@vendor_bp.route('/api/vendor', methods=['POST'])", 1)[0]
    detail_section = route_source.split("def api_get_vendor(vendor_code):", 1)[1].split("@vendor_bp.route('/api/vendor/', methods=['PUT'])", 1)[0]

    assert "get_vendor_list_payload(" in api_section
    assert "get_vendor_detail_payload(vendor_db, vendor_code)" in detail_section
    assert "session.query(VendorList)" not in api_section
    assert "session.query(VendorEmail)" not in detail_section
    assert "def get_vendor_list_payload(" in service_source
    assert "def get_vendor_detail_payload(vendor_db, vendor_code)" in service_source
    assert "'vendor_code': vendor.vendor_code" in service_source
    assert "'email_count': len(emails)" in service_source
    assert "from flask" not in service_source


def test_vendor_stockout_import_v2_is_feature_flagged_and_does_not_ship_sample_rows():
    route_source = (ROOT / "routes/vendor_routes.py").read_text(encoding="utf-8")
    template = (ROOT / "templates/vendor_stockout_import_v2.html").read_text(encoding="utf-8")
    template_function = route_source.split("def api_import_template():", 1)[1].split("@vendor_bp.route('/api/stockout/list'", 1)[0]

    assert "vendor_stockout_import_v2.html" in route_source
    assert "template_columns = [" in template_function
    assert "pd.DataFrame(columns=template_columns)" in template_function
    assert "fetchWithCSRF('/vendor-stockout/api/import/excel'" in template
    assert "vendor-stockout/api/import/excel" in template
    assert "範例" not in template_function
    assert "ui='v2'" not in template
    assert "mock" not in template.lower()
    assert "假" not in template