from __future__ import annotations

import json
import sys
import unittest
from pathlib import Path
from unittest.mock import Mock, patch

ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))

import tools.validate_snapshot_admin_web_v1 as validator
from src.quant_engine.snapshot_admin_server_v1 import (
    build_ui_state,
    fetch_domain_rows,
    fetch_table_rows,
    list_browsable_tables,
    render_collection_html,
    render_index_html,
    render_tables_html,
)
from src.quant_engine.snapshot_admin_store_v1 import import_seed_json


def _write_valid_seed(path: Path) -> None:
    payload = {
        "data": {
            "settings": [
                {"ordinal": 1, "key": "total_asset_krw", "value": 500000000, "note": "seed"},
                {"ordinal": 2, "key": "settlement_cash_d2_krw", "value": 250000000, "note": "seed"},
            ],
            "account_snapshot": [
                {
                    "captured_at": "2026-06-22T11:15:47+09:00",
                    "account": "demo",
                    "account_type": "일반계좌",
                    "ticker": "005930",
                    "name": "삼성전자",
                    "holding_quantity": 10,
                    "average_cost": 70000,
                    "parse_status": "NOT_PROVIDED",
                    "position_type": "core",
                }
            ],
        }
    }
    path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")


class TestSnapshotAdminWebV1(unittest.TestCase):

    def test_render_index_html_contains_spreadsheet_surface(self):
        html = render_index_html()
        self.assertIn("Snapshot Admin", html)
        self.assertIn("contenteditable", html)
        self.assertIn("/api/settings/save", html)
        self.assertIn("/api/account_snapshot/save", html)
        self.assertIn("opsBanner", html)
        self.assertIn("bannerApprovalSummary", html)
        self.assertIn("heroNextAction", html)
        self.assertIn("heroPrimaryAction", html)
        self.assertIn("heroCollection", html)
        self.assertIn("approvalPanel", html)
        self.assertIn("collectionPanel", html)
        self.assertIn("selectionPanel", html)
        self.assertIn("open when you need row-level operations", html)
        self.assertIn("open", html)
        self.assertIn("snapshot-panel", html)
        self.assertIn("selected-field", html)
        self.assertIn("settingsCountChip", html)
        self.assertIn("snapshotCountChip", html)
        self.assertIn("Lock target", html)
        self.assertIn("Lock row", html)
        self.assertIn("Approve pending", html)
        self.assertIn("Refresh diff", html)
        self.assertIn("Export approval packet", html)
        self.assertIn("Selection Inspector", html)
        self.assertIn("Recent row history", html)
        self.assertIn("Recent change summary", html)
        self.assertIn("Save view", html)
        self.assertIn("Apply TSV to selection", html)
        self.assertIn("Ctrl+S", html)
        self.assertIn("KIS Collection", html)
        self.assertIn("Recent collector snapshots", html)
        self.assertIn("Collection detail", html)
        self.assertIn("Filter runs / snapshots / errors", html)
        self.assertIn("Filter change log", html)
        self.assertIn("Timeline", html)
        self.assertIn("/collection", html)
        self.assertIn("Open collection dashboard", html)

    def test_render_home_html_contains_role_based_entrances(self):
        from src.quant_engine.snapshot_admin_server_v1 import render_home_html

        html = render_home_html()
        self.assertIn("Snapshot Admin Home", html)
        self.assertIn("1. Workspace", html)
        self.assertIn("2. Collection", html)
        self.assertIn("3. Tables", html)
        self.assertIn("Open workspace", html)
        self.assertIn("Open collection", html)
        self.assertIn("Open tables", html)
        self.assertIn("/workspace", html)
        self.assertIn("/tables", html)

    def test_render_tables_html_contains_table_group_summary(self):
        html = render_tables_html()
        self.assertIn("Snapshot Admin — Table Browser", html)
        self.assertIn("DB별 수정, JSON별 검토, 수집 증빙 확인", html)
        self.assertIn("DB 먼저 / JSON은 증빙", html)
        self.assertIn("tablePurposeNavigator", html)
        self.assertIn("DB 먼저", html)
        self.assertIn("JSON은 증빙", html)
        self.assertIn("Edit only canonical rows", html)
        self.assertIn("DB tables", html)
        self.assertIn("Editable source of truth.", html)
        self.assertIn("Workbook sheets", html)
        self.assertIn("Derived report evidence.", html)
        self.assertIn("tablesVersionTop", html)
        self.assertIn("tablesVersionBottom", html)
        self.assertIn("final_updated_at=", html)
        self.assertIn("tablesCoverageWarning", html)
        self.assertIn("workbookSheetSelect", html)
        self.assertIn("Workbook sheets only", html)
        self.assertIn("workbookSheetMeta", html)
        self.assertIn("workbookSheetSurfaceMeta", html)
        self.assertIn("workbookSheetDetail", html)
        self.assertIn("workbookSheetPreview", html)
        self.assertIn("tableSelect", html)
        self.assertIn("DB Table", html)
        self.assertIn("tableGroupSummary", html)
        self.assertIn("tableSourceSummary", html)
        self.assertIn("Registry details", html)
        self.assertIn("History details", html)
        self.assertIn("Workspace", html)
        self.assertIn("Collection", html)
        self.assertIn("Strategy", html)
        self.assertIn("JSON", html)
        self.assertIn("tableWorkspaceSection", html)
        self.assertIn("tableJsonSection", html)
        self.assertIn("tableEditState", html)
        self.assertIn("bg-danger-lt", html)
        self.assertIn("Save applies only to the canonical workspace DB.", html)
        self.assertIn("Derived JSON Evidence Preview", html)
        self.assertIn("jsonReportStatus", html)
        self.assertIn("jsonReportFocus", html)
        self.assertIn("jsonReportStats", html)
        self.assertIn("jsonReportDetail", html)
        self.assertIn("Purpose: edit canonical workspace rows only.", html)
        self.assertIn("Collection purpose: monitor collector runs and snapshots.", html)
        self.assertIn("Latest run summary loads from `/api/state`.", html)
        self.assertIn("Purpose: inspect DB-backed JSON evidence and row payloads.", html)
        self.assertIn("Workspace tables are editable only when the table is in the canonical workspace DB.", html)
        self.assertIn("DB별 / JSON별 조회 기준", html)
        self.assertIn("This page is intentionally a triage surface, not a generic table dump.", html)
        self.assertIn("read-only because this table belongs to collector or strategy storage", html)
        self.assertIn("Table Browser", html)
        self.assertIn("Save changes", html)
        self.assertIn("Clear filters", html)
        self.assertIn("• current", html)
        self.assertIn("workbookRegistrySummary", html)
        self.assertIn("dbRegistryBody", html)
        self.assertIn("Derived report registry", html)
        self.assertIn("DB tables", html)
        self.assertIn("workbookPurposeFilters", html)
        self.assertIn("Recorded surface", html)
        self.assertIn("History details", html)
        self.assertIn("historyChangeBody", html)
        self.assertIn("historyRunBody", html)
        self.assertIn("Derived JSON evidence view", html)
        self.assertIn("Derived JSON Evidence Preview", html)
        self.assertIn("JSON evidence", html)

    def test_render_tables_html_exposes_workbook_registry_surface(self):
        html = render_tables_html()
        self.assertIn("Workbook sheets", html)
        self.assertIn("XLSX:", html)
        self.assertIn("JSON evidence:", html)
        self.assertIn("JSON role:", html)
        self.assertIn("Unmapped:", html)
        self.assertIn("Purpose filter:", html)
        self.assertIn("destination:", html)
        self.assertIn("recorded surface:", html)
        self.assertIn("Selected sheet:", html)
        self.assertIn("surface:", html)
        self.assertIn("version=", html)
        self.assertIn("No workbook sheet selected.", html)
        self.assertIn("tableSelect", html)
        self.assertIn("DB tables only", html)
        self.assertIn("Registry details", html)

    def test_workbook_registry_maps_xlsx_sheets_to_recording_surfaces(self):
        from src.quant_engine.snapshot_admin_server_v1 import load_workbook_sheet_registry

        registry = load_workbook_sheet_registry()
        self.assertEqual(registry["sheet_count"], 19)
        entries = {row["sheet"]: row for row in registry["entries"]}
        self.assertEqual(entries["settings"]["kind"], "workspace_db")
        self.assertEqual(entries["account_snapshot"]["kind"], "workspace_db")
        self.assertEqual(entries["data_feed"]["kind"], "collector_db")
        self.assertEqual(entries["settings"]["purpose"], "workspace_edit")
        self.assertEqual(entries["data_feed"]["purpose"], "collector_run")
        self.assertEqual(registry["json_role"], "derived_report_evidence")
        for sheet in [
            "sector_universe_refresh_audit",
            "daily_history",
            "event_calendar",
            "pa1_feedback",
            "alpha_history",
            "backdata_feature_bank",
            "sell_priority",
            "harness_context",
            "monthly_history",
            "sector_flow_history",
            "sector_universe",
            "core_satellite",
            "universe",
            "event_risk",
            "macro",
            "sector_flow",
        ]:
            self.assertEqual(entries[sheet]["kind"], "json_payload")
            self.assertEqual(entries[sheet]["destination"], "GatherTradingData.json")
            self.assertEqual(entries[sheet]["source_role"], "derived_report_evidence")
        self.assertEqual(entries["sector_universe_refresh_audit"]["kind"], "json_payload")
        self.assertEqual(entries["daily_history"]["kind"], "json_payload")
        self.assertEqual(entries["sector_universe_refresh_audit"]["purpose"], "refresh_audit")
        self.assertEqual(entries["daily_history"]["purpose"], "history_ledger")
        self.assertEqual(entries["event_calendar"]["purpose"], "audit_history")
        self.assertEqual(entries["alpha_history"]["purpose"], "analysis_report")
        self.assertEqual(entries["harness_context"]["purpose"], "execution_context")
        self.assertEqual(entries["monthly_history"]["purpose"], "history_ledger")
        self.assertEqual(entries["sector_universe"]["purpose"], "universe_registry")
        self.assertEqual(entries["event_risk"]["purpose"], "macro_risk_context")
        self.assertEqual(entries["sector_flow"]["purpose"], "flow_leadership")
        self.assertNotIn("sector_universe_refresh_audit", registry["unmapped_sheets"])
        self.assertNotIn("daily_history", registry["unmapped_sheets"])

    def test_render_collection_html_contains_dashboard_surface(self):
        html = render_collection_html()
        self.assertIn("KIS Collection Dashboard", html)
        self.assertIn("/api/state", html)
        self.assertIn("/api/collection/run", html)
        self.assertIn("Collect now", html)
        self.assertIn("collectionModeInput", html)
        self.assertIn("collectionAccountInput", html)
        self.assertIn("collectionRunLog", html)
        self.assertIn("collectionRunBanner", html)
        self.assertIn("collectionModeBadge", html)
        self.assertIn("collectionLiveStatus", html)
        self.assertIn("collectionProgressChip", html)
        self.assertIn("collectionStageChip", html)
        self.assertIn("collectionResultChip", html)
        self.assertIn("collectionTrendSummary", html)
        self.assertIn("collectionTrendChart", html)
        self.assertIn("Auto refreshes every 15 seconds", html)
        self.assertIn("older → newer", html)
        self.assertIn("Snapshots / run", html)
        self.assertIn("Errors / run", html)
        self.assertIn("[RUN]", html)
        self.assertIn("[SNAPSHOT]", html)
        self.assertIn("[ERROR]", html)
        self.assertIn("live source: active | KIS rows=", html)
        self.assertIn("collectionDetailAnchor", html)
        self.assertIn("collectionRunLogAnchor", html)
        self.assertIn("Live KIS on", html)
        self.assertIn("live source: unknown", html)
        self.assertNotIn('value="mock"', html)
        self.assertIn("Download raw JSON", html)
        self.assertIn("Download CSV", html)
        self.assertIn("Filter runs / snapshots / errors", html)
        self.assertIn("Unified activity timeline", html)
        self.assertIn("date", html)
        self.assertIn("Ticker quick search", html)
        self.assertIn("Date quick search", html)

    def test_run_collection_job_returns_progress_payload(self):
        import tempfile
        import shutil
        from src.quant_engine.snapshot_admin_server_v1 import KIS_COLLECTION_DB, KIS_COLLECTION_REPORT, run_collection_job

        tmp_dir = tempfile.mkdtemp()
        try:
            sqlite_db = Path(tmp_dir) / "kis_data_collection.db"
            output_json = Path(tmp_dir) / "kis_data_collection_v1.json"
            output_json.write_text(
                json.dumps(
                    {
                        "generated_at": "2026-06-24T10:00:00+09:00",
                        "row_count": 1,
                        "source_counts": {"kis_open_api": 1},
                    },
                    ensure_ascii=False,
                    indent=2,
                ),
                encoding="utf-8",
            )
            fake_proc = Mock(returncode=0, stdout="ok", stderr="")
            with patch("src.quant_engine.snapshot_admin_server_v1.subprocess.run", return_value=fake_proc) as run_mock:
                payload = run_collection_job(
                    sqlite_db=sqlite_db,
                    input_json=Path(tmp_dir) / "GatherTradingData.json",
                    output_json=output_json,
                    kis_account="real",
                    include_live_kis=True,
                    allow_naver_fallback=False,
                )

            self.assertEqual(payload["status"], "PASS")
            self.assertEqual(payload["returncode"], 0)
            self.assertEqual(payload["stdout"], "ok")
            self.assertIn("started_at", payload)
            self.assertIn("finished_at", payload)
            self.assertIn("elapsed_ms", payload)
            self.assertEqual(payload["summary"]["row_count"], 1)
            self.assertEqual(payload["summary"]["source_counts"]["kis_open_api"], 1)
            self.assertEqual(payload["state"]["db_path"], str(sqlite_db))
            run_mock.assert_called_once()
            self.assertTrue(str(KIS_COLLECTION_DB).endswith("kis_data_collection.db"))
            self.assertTrue(str(KIS_COLLECTION_REPORT).endswith("kis_data_collection_v1.json"))
        finally:
            shutil.rmtree(tmp_dir, ignore_errors=True)

    def test_build_ui_state_exposes_expected_columns(self):
        import tempfile
        import shutil
        tmp_dir = tempfile.mkdtemp()
        try:
            db_path = Path(tmp_dir) / "snapshot_admin.db"
            seed_path = Path(tmp_dir) / "valid_seed.json"
            _write_valid_seed(seed_path)
            import_seed_json(db_path, seed_path)

            state = build_ui_state(db_path)
            self.assertTrue(state["summary"]["settings_rows"] > 0)
            self.assertTrue(state["summary"]["account_snapshot_rows"] > 0)
            self.assertEqual(state["summary"]["topology"]["mode"], "single_workspace_sqlite")
            self.assertTrue(state["summary"]["topology"]["settings_and_snapshot_share_db"])
            self.assertTrue(state["summary"]["topology"]["collector_separate_db"])
            self.assertEqual(state["account_snapshot_columns"][0], "captured_at")
            self.assertIn("settings", state["validation"])
            self.assertTrue(state["version"]["app"])
            self.assertIn("fingerprint", state["version"]["source"])
            self.assertIn("collection", state)
            self.assertIn("counts", state["collection"])
            self.assertIn("latest_report", state["collection"])
            self.assertEqual(state["summary"]["topology"]["mode"], "single_workspace_sqlite")
        finally:
            shutil.rmtree(tmp_dir, ignore_errors=True)

    def test_snapshot_admin_workflow_and_script_exist(self):
        workflow = ROOT / ".gitea" / "workflows" / "snapshot_admin.yml"
        package = json.loads((ROOT / "package.json").read_text(encoding="utf-8"))
        self.assertTrue(workflow.exists())
        self.assertIn("--reload", package["scripts"]["ops:snapshot-web"])
        self.assertIn("--reload", package["scripts"]["ops:snapshot-web-watch"])
        self.assertIn("ops:snapshot-validate", package["scripts"])
        self.assertIn("ops:snapshot-web-validate", package["scripts"])

    def test_render_tables_html_contains_tabler_grid_surface(self):
        html = render_tables_html()
        self.assertIn("tabler", html.lower())
        self.assertIn("tableSelect", html)
        self.assertIn("/api/tables", html)
        self.assertIn("/api/table_rows", html)
        self.assertIn("/api/domain_rows", html)
        self.assertIn("saveCurrentTable", html)
        self.assertIn("gridTable", html)
        self.assertIn("gridFilter", html)
        self.assertIn("gridFilterRow", html)
        self.assertIn("Clear filters", html)
        self.assertIn("tableBannerDetail", html)

    def test_list_browsable_tables_covers_all_three_databases(self):
        import tempfile
        import shutil
        tmp_dir = tempfile.mkdtemp()
        try:
            db_path = Path(tmp_dir) / "snapshot_admin.db"
            seed_path = Path(tmp_dir) / "valid_seed.json"
            _write_valid_seed(seed_path)
            import_seed_json(db_path, seed_path)

            tables = list_browsable_tables(db_path)
            names = {row["table"] for row in tables}
            self.assertEqual(tables[0]["table"], "account_snapshot")
            self.assertEqual(tables[1]["table"], "settings")
            self.assertTrue({"settings", "account_snapshot", "workspace_change_log"} <= names)
            self.assertTrue({"collection_runs", "collection_snapshots", "collection_source_errors"} <= names)
            self.assertTrue({"sell_strategy_results", "satellite_recommendations"} <= names)

            settings_row = next(row for row in tables if row["table"] == "settings")
            self.assertTrue(settings_row["exists"])
            self.assertTrue(settings_row["row_count"] > 0)
        finally:
            shutil.rmtree(tmp_dir, ignore_errors=True)

    def test_fetch_table_rows_paginates_and_rejects_unknown_table(self):
        import tempfile
        import shutil
        tmp_dir = tempfile.mkdtemp()
        try:
            db_path = Path(tmp_dir) / "snapshot_admin.db"
            seed_path = Path(tmp_dir) / "valid_seed.json"
            _write_valid_seed(seed_path)
            import_seed_json(db_path, seed_path)

            page1 = fetch_table_rows("settings", db_path, limit=2, offset=0)
            self.assertTrue(page1["columns"])
            self.assertEqual(len(page1["rows"]), 2)
            self.assertTrue(page1["total"] >= 2)

            page2 = fetch_table_rows("settings", db_path, limit=2, offset=2)
            self.assertNotEqual(page1["rows"], page2["rows"])

            filtered = fetch_table_rows("settings", db_path, limit=50, offset=0, filter_text="total_asset_krw")
            self.assertEqual(filtered["total"], 1)
            self.assertEqual(len(filtered["rows"]), 1)

            with self.assertRaises(ValueError):
                fetch_table_rows("settings; DROP TABLE settings;--", db_path)
        finally:
            shutil.rmtree(tmp_dir, ignore_errors=True)

    def test_fetch_domain_rows_exposes_editable_tables(self):
        import tempfile
        import shutil
        tmp_dir = tempfile.mkdtemp()
        try:
            db_path = Path(tmp_dir) / "snapshot_admin.db"
            seed_path = Path(tmp_dir) / "valid_seed.json"
            _write_valid_seed(seed_path)
            import_seed_json(db_path, seed_path)

            settings = fetch_domain_rows("settings", db_path)
            snapshot = fetch_domain_rows("account_snapshot", db_path)
            self.assertEqual(settings["domain"], "settings")
            self.assertTrue(settings["rows"])
            self.assertEqual(snapshot["domain"], "account_snapshot")
            self.assertTrue(snapshot["rows"])

            with self.assertRaises(ValueError):
                fetch_domain_rows("workspace_change_log", db_path)
        finally:
            shutil.rmtree(tmp_dir, ignore_errors=True)


    def test_snapshot_admin_web_validation_script_passes(self):
        out = ROOT / "Temp" / "snapshot_admin_web_validation_v1.json"
        if out.exists():
            out.unlink()

        rc = validator.main()
        payload = json.loads(out.read_text(encoding="utf-8"))

        self.assertEqual(rc, 0)
        self.assertEqual(payload["gate"], "PASS")
        self.assertEqual(payload["formula_id"], "SNAPSHOT_ADMIN_WEB_VALIDATION_V1")
        self.assertTrue(payload["settings_rows"] > 0)
        self.assertTrue(payload["account_snapshot_rows"] > 0)


if __name__ == "__main__":
    unittest.main()

