"""WBS-7.7 — KIS 수집 → 스냅샷 어드민 적재 → 정성매도전략 평가 E2E 체인.

단위 테스트(tests/unit)는 각 모듈을 독립적으로 검증하지만, 모듈 간 실제 데이터
경로(kis_data_collection_v1 → data_collection_store_v1.db → snapshot_admin의
collection dashboard / qualitative_sell_strategy_v1 → qualitative_sell_strategy_store_v1.db)
를 연결해서 검증하는 테스트가 없었다(2026-06-21 비판적 리뷰 0c절, WBS-7.7).

이 테스트는 네트워크를 전혀 사용하지 않는다(--no-live-kis --no-naver와 동일한 경로,
또는 Naver 호출을 명시적으로 예외 처리시켜 graceful degradation을 검증).
"""
from __future__ import annotations

import json
import sys
import urllib.request
import socket
from datetime import date
from pathlib import Path

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

import unittest

import tools.validate_platform_transition_wbs_v1 as platform_transition_validator
import tools.validate_snapshot_admin_web_v1 as snapshot_admin_validator
from src.quant_engine import kis_data_collection_v1 as kdc
from src.quant_engine.data_collection_store_v1 import load_collection_dashboard_state
from src.quant_engine.qualitative_sell_strategy_v1 import compute_qualitative_sell_strategy
from src.quant_engine.qualitative_sell_strategy_store_v1 import (
    fetch_recent_sell_strategy_results,
    insert_sell_strategy_result,
)

SEED_ROWS = [
    {"Ticker": "005930", "Name": "삼성전자", "Sector": "반도체"},
    {"Ticker": "000660", "Name": "SK하이닉스", "Sector": "반도체"},
]


def _write_seed_json(path: Path) -> Path:
    path.write_text(json.dumps({"data": {"data_feed": SEED_ROWS}}, ensure_ascii=False), encoding="utf-8")
    return path


class TestKisCollectionIntegration(unittest.TestCase):

    def test_kis_collection_writes_sqlite_that_snapshot_admin_dashboard_reads_back(self):
        """1단계: KIS 수집(네트워크 미사용) → SQLite 적재 → snapshot_admin 대시보드 read-back."""
        import tempfile

        with tempfile.TemporaryDirectory() as tmpdir:
            tmp_path = Path(tmpdir)
            seed_json = _write_seed_json(tmp_path / "seed.json")
            db_path = tmp_path / "data_collection_store_v1.db"
            output_json = tmp_path / "kis_data_collection_v1.json"

            summary = kdc.collect_to_sqlite(
                input_json=seed_json,
                sqlite_db=db_path,
                output_json=output_json,
                kis_account="mock",
                include_naver=False,
                include_live_kis=False,
            )

            self.assertIn(summary["status"], {"PASS", "PASS_WITH_WARNINGS"})
            self.assertEqual(summary["row_count"], len(SEED_ROWS))
            self.assertFalse(summary["errors"])

            dashboard = load_collection_dashboard_state(db_path, output_json)
            self.assertGreaterEqual(dashboard["counts"]["collection_runs"], 1)
            self.assertEqual(dashboard["counts"]["collection_snapshots"], len(SEED_ROWS))
            self.assertEqual(dashboard["counts"]["collection_source_errors"], 0)
            tickers_in_dashboard = {row["ticker"] for row in dashboard["recent_snapshots"]}
            self.assertTrue({"005930", "000660"} <= tickers_in_dashboard)

    def test_naver_fetch_exception_degrades_gracefully_without_breaking_batch(self):
        """Cloudflare 403 등 Naver 폴백 차단 시 graceful degradation 검증."""
        import tempfile

        def _raise_cloudflare_block(_session, _code):
            raise RuntimeError("HTTP 403 Forbidden (Cloudflare)")

        original_fetch = kdc.fetch_price_history
        original_session = kdc.naver_session
        kdc.fetch_price_history = _raise_cloudflare_block
        kdc.naver_session = lambda: object()
        try:
            with tempfile.TemporaryDirectory() as tmpdir:
                tmp_path = Path(tmpdir)
                seed_json = _write_seed_json(tmp_path / "seed.json")
                db_path = tmp_path / "data_collection_store_v1.db"
                output_json = tmp_path / "kis_data_collection_v1.json"
                summary = kdc.collect_to_sqlite(
                    input_json=seed_json,
                    sqlite_db=db_path,
                    output_json=output_json,
                    kis_account="mock",
                    include_naver=True,
                    include_live_kis=False,
                )
                self.assertIn(summary["status"], {"PASS", "PASS_WITH_WARNINGS"})
                self.assertEqual(summary["row_count"], len(SEED_ROWS))
                self.assertFalse(summary["errors"])
        finally:
            kdc.fetch_price_history = original_fetch
            kdc.naver_session = original_session

    def test_seed_rows_and_price_source_helpers_are_deterministic(self):
        import tempfile

        with tempfile.TemporaryDirectory() as tmpdir:
            tmp_path = Path(tmpdir)
            seed_json = _write_seed_json(tmp_path / "seed.json")
            rows = kdc._build_seed_rows(seed_json)
            self.assertEqual([row["Ticker"] for row in rows], ["005930", "000660"])

            original_kis = kdc._normalize_kis_fields
            original_naver = kdc._normalize_naver_price_history
            kdc._normalize_kis_fields = lambda ticker, account: {"status": "OK", "current_price": 70000, "volume": 1234}
            kdc._normalize_naver_price_history = lambda ticker: {"status": "OK", "close": 65000, "volume": 1111}
            try:
                kis, naver, source_priority = kdc._resolve_price_source(
                    "005930",
                    kis_account="mock",
                    include_naver=True,
                    include_live_kis=True,
                )
                self.assertEqual(kis["status"], "OK")
                self.assertEqual(naver["status"], "OK")
                self.assertIn("kis_open_api", source_priority)
                self.assertIn("naver_finance", source_priority)
                self.assertEqual(source_priority[0], "kis_open_api")

                normalized = {"Ticker": "005930", "Name": "삼성전자", "Sector": "반도체"}
                kdc._apply_source_fallbacks(normalized, row=normalized, kis=kis, naver=naver)
                self.assertEqual(normalized["current_price"], 70000)
                self.assertEqual(normalized["volume"], 1234)
            finally:
                kdc._normalize_kis_fields = original_kis
                kdc._normalize_naver_price_history = original_naver

    def test_qualitative_sell_strategy_decision_round_trips_through_store(self):
        """2단계: 정성매도전략 평가(순수 함수, 네트워크 미사용) → SQLite 저장 → 조회 round-trip."""
        ctx = {
            "today": date(2026, 6, 21),
            "macro_pressure": 0.5,
            "fundamental_trajectory": 0.4,
            "short_interest_pressure": 0.6,
            "microstructure_pressure": 0.2,
            "liquidity_rotation_risk": 0.5,
            "rate_trend": "RISING",
        }
        decision = compute_qualitative_sell_strategy(ctx)
        self.assertIn(decision["action"], {
            "EXIT_REVIEW_FULL",
            "TRIM_REVIEW_PARTIAL",
            "HOLD_ADD_CONVICTION",
            "HOLD_NO_CONFLUENCE",
            "INSUFFICIENT_DATA_NO_ACTION",
        })

        result = {
            "code": "005930",
            "generated_at": "2026-06-21T15:30:00+09:00",
            "decision": decision,
        }

        import tempfile

        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = Path(tmpdir) / "qualitative_sell_strategy.db"
            insert_sell_strategy_result(db_path, result)
            fetched = fetch_recent_sell_strategy_results(db_path, "005930", limit=5)
            self.assertEqual(len(fetched), 1)
            self.assertEqual(fetched[0]["code"], "005930")
            self.assertEqual(fetched[0]["action"], decision["action"])
            self.assertEqual(fetched[0]["conviction"], decision["conviction"])
            self.assertEqual(fetched[0]["market_regime"], decision["market_regime"])

    def test_snapshot_admin_and_platform_transition_validators_remain_passable(self):
        """E2E 체인 결과물이 snapshot_admin 및 platform-transition 검증에 그대로 연결되는지 확인."""
        snapshot_out = ROOT / "Temp" / "snapshot_admin_web_validation_v1.json"
        transition_out = ROOT / "Temp" / "platform_transition_wbs_v1.json"
        if snapshot_out.exists():
            snapshot_out.unlink()
        if transition_out.exists():
            transition_out.unlink()

        snapshot_rc = snapshot_admin_validator.main()
        transition_rc = platform_transition_validator.main()

        snapshot_payload = json.loads(snapshot_out.read_text(encoding="utf-8"))
        transition_payload = json.loads(transition_out.read_text(encoding="utf-8"))

        self.assertEqual(snapshot_rc, 0)
        self.assertEqual(snapshot_payload["gate"], "PASS")
        self.assertGreater(snapshot_payload["settings_rows"], 0)
        self.assertGreater(snapshot_payload["account_snapshot_rows"], 0)

        self.assertEqual(transition_rc, 0)
        self.assertEqual(transition_payload["gate"], "PASS")
        self.assertFalse(transition_payload["missing_criteria"])
        self.assertFalse(transition_payload["roadmap_missing"])
        self.assertTrue(transition_payload["checks"]["P1_kis_core_api_collector"]["gate"] == "PASS")
        self.assertTrue(transition_payload["checks"]["P2_sqlite_canonical_store"]["gate"] == "PASS")
        self.assertTrue(transition_payload["checks"]["P3_ci_scheduler_cutover"]["gate"] == "PASS")
        self.assertTrue(transition_payload["checks"]["P4_gas_thin_adapter_minimize"]["gate"] == "PASS")
        self.assertTrue(transition_payload["checks"]["P5_postgresql_upgrade_path"]["gate"] == "PASS")

    def test_snapshot_admin_collection_run_updates_state_with_live_price(self):
        """실제 수집 후 snapshot_admin state가 최신 run 및 live price를 반영하는지 확인."""
        import tempfile
        import subprocess
        import time

        with tempfile.TemporaryDirectory() as tmpdir:
            tmp_path = Path(tmpdir)
            db_path = tmp_path / "snapshot_admin.db"
            seed_path = tmp_path / "seed.json"
            seed_payload = {
                "data": {
                    "settings": [
                        {"ordinal": 1, "key": "total_asset_krw", "value": 500000000, "note": "seed"},
                        {"ordinal": 2, "key": "settlement_cash_d2_krw", "value": 250000000, "note": "seed"},
                    ],
                    "data_feed": [
                        {"Ticker": "005930", "Name": "삼성전자", "Sector": "반도체"},
                        {"Ticker": "000660", "Name": "SK하이닉스", "Sector": "반도체"},
                    ],
                    "account_snapshot": [
                        {
                            "captured_at": "2026-06-24T11:15:47+09:00",
                            "account": "real",
                            "account_type": "일반계좌",
                            "ticker": "005930",
                            "name": "삼성전자",
                            "holding_quantity": 10,
                            "available_quantity": 10,
                            "average_cost": 70000,
                            "total_cost": 700000,
                            "current_price": 71000,
                            "market_value": 710000,
                            "profit_loss": 10000,
                            "return_pct": 1.43,
                            "immediate_cash": 0,
                            "settlement_cash_d2": 0,
                            "available_cash": 0,
                            "open_order_amount": 0,
                            "monthly_contribution_limit": 0,
                            "monthly_contribution_used": 0,
                            "parse_status": "CAPTURE_PROVIDED_BUT_NOT_HOLDINGS",
                            "user_confirmed": "N",
                            "stop_price": 65000,
                            "highest_price_since_entry": 72000,
                            "entry_date": "2026-06-01",
                            "entry_stage": "stage_1",
                            "position_type": "core",
                            "last_updated": "2026-06-24T11:15:47+09:00",
                        }
                    ],
                }
            }
            seed_path.write_text(json.dumps(seed_payload, ensure_ascii=False, indent=2), encoding="utf-8")
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
                sock.bind(("127.0.0.1", 0))
                port = int(sock.getsockname()[1])

            proc = subprocess.Popen(
                [
                    sys.executable,
                    "-m",
                    "src.quant_engine.snapshot_admin_server_v1",
                    "--host",
                    "127.0.0.1",
                    "--port",
                    str(port),
                    "--db",
                    str(db_path),
                    "--seed",
                    str(seed_path),
                ],
                cwd=ROOT,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                encoding="utf-8",
            )
            try:
                base_url = f"http://127.0.0.1:{port}"
                deadline = time.time() + 20
                while time.time() < deadline:
                    if proc.poll() is not None:
                        output = proc.stdout.read() if proc.stdout else ""
                        self.fail(f"snapshot_admin server exited early with code {proc.returncode}:\n{output}")
                    try:
                        with urllib.request.urlopen(f"{base_url}/api/state", timeout=2) as response:
                            json.loads(response.read().decode("utf-8"))
                        break
                    except Exception:
                        time.sleep(0.25)
                else:
                    output = proc.stdout.read() if proc.stdout else ""
                    self.fail(f"snapshot_admin server did not become ready:\n{output}")

                request = urllib.request.Request(
                    f"{base_url}/api/collection/run",
                    data=json.dumps(
                        {
                            "mode": "real",
                            "kis_account": "real",
                            "include_live_kis": True,
                            "allow_naver_fallback": False,
                        },
                        ensure_ascii=False,
                    ).encode("utf-8"),
                    headers={"Content-Type": "application/json"},
                    method="POST",
                )
                with urllib.request.urlopen(request, timeout=120) as response:
                    run_payload = json.loads(response.read().decode("utf-8"))

                with urllib.request.urlopen(f"{base_url}/api/state", timeout=10) as response:
                    state_payload = json.loads(response.read().decode("utf-8"))

                collection = state_payload.get("collection", {})
                latest_report = collection.get("latest_report", {})
                latest_run = collection.get("latest_run", {})
                recent_runs = collection.get("runs", [])

                self.assertEqual(run_payload["status"], "PASS")
                self.assertTrue(any(run.get("run_id") == run_payload["summary"]["run_id"] for run in recent_runs))
                self.assertIn(latest_report.get("run_id"), {run_payload["summary"]["run_id"], latest_run.get("run_id")})
                self.assertGreaterEqual(latest_report["source_counts"]["kis_open_api"], 1)
                self.assertGreaterEqual(len(latest_report.get("rows", [])), 1)
                self.assertIsNotNone(latest_report["rows"][0].get("current_price"))
            finally:
                proc.terminate()
                try:
                    proc.wait(timeout=10)
                except Exception:
                    proc.kill()
