import tempfile
import unittest
from unittest import mock
from contextlib import redirect_stdout
from io import StringIO
from pathlib import Path
import uuid
import os
import json
import urllib.request
import re
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import threading

from hiveos.intelligence import tracing as tracing_module
from hiveos import config as config_module
from hiveos import hive as hive_module
from hiveos.observe import (
    observe_checkpoint,
    observe_rerun_request,
    observe_run,
    observe_proxy,
    observe_step,
    run_command_observed,
    main as observe_main,
)


class ObserveHarnessTests(unittest.TestCase):
    def setUp(self):
        self._original_trace_db_async = tracing_module.TRACE_DB_ASYNC
        self._original_trace_sampling_enabled = tracing_module.TRACE_SAMPLING_ENABLED
        self._original_trace_log_path = tracing_module.TRACE_LOG_PATH
        self._original_trace_home = os.environ.get("HIVE_TRACE_HOME")
        self._original_disable_browser = os.environ.get("HIVE_TRACE_DISABLE_BROWSER")
        self._original_reconcile_enabled = config_module.TRACE_RECONCILE_ENABLED
        self._original_reconcile_auto_on_read = config_module.TRACE_RECONCILE_AUTO_ON_READ
        self._original_reconcile_stale_sec = config_module.TRACE_RECONCILE_STALE_SEC
        self._original_open_on_run = config_module.TRACE_OPEN_ON_RUN

        self._trace_log_path = str(Path(tempfile.gettempdir()) / f"hive-observe-{uuid.uuid4().hex}.log")
        self._trace_home = str(Path(tempfile.gettempdir()) / f"hive-observe-home-{uuid.uuid4().hex}")
        tracing_module.TRACE_LOG_PATH = self._trace_log_path
        tracing_module.TRACE_DB_ASYNC = False
        tracing_module.TRACE_SAMPLING_ENABLED = False
        os.environ["HIVE_TRACE_HOME"] = self._trace_home
        os.environ["HIVE_TRACE_DISABLE_BROWSER"] = "1"
        config_module.TRACE_RECONCILE_ENABLED = True
        config_module.TRACE_RECONCILE_AUTO_ON_READ = True
        config_module.TRACE_RECONCILE_STALE_SEC = 300
        config_module.TRACE_OPEN_ON_RUN = False

    def tearDown(self):
        tracing_module.TRACE_DB_ASYNC = self._original_trace_db_async
        tracing_module.TRACE_SAMPLING_ENABLED = self._original_trace_sampling_enabled
        tracing_module.TRACE_LOG_PATH = self._original_trace_log_path
        if self._original_trace_home is None:
            os.environ.pop("HIVE_TRACE_HOME", None)
        else:
            os.environ["HIVE_TRACE_HOME"] = self._original_trace_home
        if self._original_disable_browser is None:
            os.environ.pop("HIVE_TRACE_DISABLE_BROWSER", None)
        else:
            os.environ["HIVE_TRACE_DISABLE_BROWSER"] = self._original_disable_browser
        config_module.TRACE_RECONCILE_ENABLED = self._original_reconcile_enabled
        config_module.TRACE_RECONCILE_AUTO_ON_READ = self._original_reconcile_auto_on_read
        config_module.TRACE_RECONCILE_STALE_SEC = self._original_reconcile_stale_sec
        config_module.TRACE_OPEN_ON_RUN = self._original_open_on_run
        self._cleanup_trace_files()
        self._cleanup_trace_home()

    def _cleanup_trace_files(self):
        base = Path(self._trace_log_path)
        for path in [base] + list(base.parent.glob(f"{base.name}.*")):
            try:
                if path.exists():
                    path.unlink()
            except Exception:
                pass

    def _cleanup_trace_home(self):
        home = Path(self._trace_home)
        if not home.exists():
            return
        for path in sorted(home.rglob("*"), reverse=True):
            try:
                if path.is_file():
                    path.unlink()
                elif path.is_dir():
                    path.rmdir()
            except Exception:
                pass

    def test_observe_run_context_emits_start_step_finish(self):
        with observe_run("unit-test-run", metadata={"suite": "observe"}) as run_id:
            observe_step(run_id, "step-alpha", payload={"ok": True})

        started = tracing_module.read_trace_events(limit=200, filters={"event_type": "observe_run_started"})
        stepped = tracing_module.read_trace_events(limit=200, filters={"event_type": "observe_step"})
        finished = tracing_module.read_trace_events(limit=200, filters={"event_type": "observe_run_finished"})

        self.assertTrue(started, "expected observe_run_started event")
        self.assertTrue(stepped, "expected observe_step event")
        self.assertTrue(finished, "expected observe_run_finished event")
        self.assertEqual("success", finished[-1].get("outcome"))

    def test_run_command_observed_emits_cli_events(self):
        result = run_command_observed(
            ["python", "-c", "print('hello-observe')"],
            run_name="cli-test",
            open_ui_url="",
        )
        self.assertEqual(0, result["exit_code"])
        self.assertIn("run_id", result)

        started = tracing_module.read_trace_events(limit=200, filters={"event_type": "observe_run_started"})
        finished = tracing_module.read_trace_events(limit=200, filters={"event_type": "observe_run_finished"})
        self.assertTrue(started)
        self.assertTrue(finished)
        self.assertEqual("success", finished[-1].get("outcome"))

    def test_run_command_observed_tolerates_non_utf_output_bytes(self):
        # Emit a raw byte sequence that can fail locale decoding on Windows.
        result = run_command_observed(
            ["python", "-c", "import sys; sys.stdout.buffer.write(bytes([0x8f, 0x0a])); sys.stdout.flush()"],
            run_name="decode-tolerance-test",
            open_ui_url="",
            timeout_sec=5,
        )
        self.assertEqual(0, result["exit_code"])

    def test_hive_module_alias_runs_trace_command(self):
        run_command_observed(["python", "-c", "pass"], run_name="hive-alias-test", open_ui_url="")

        buf = StringIO()
        with redirect_stdout(buf):
            code = hive_module.main(["trace", "ls", "--limit", "5"])
        self.assertEqual(0, code)
        self.assertIn("observe-run:", buf.getvalue())

    def test_checkpoint_and_rerun_sdk_events(self):
        with observe_run("checkpoint-test") as run_id:
            observe_checkpoint(run_id, "ckpt-1", step_name="stage.extract", state_ref="state://extract/1")
            observe_rerun_request(run_id, from_checkpoint_id="ckpt-1", reason="retry_with_override")

        checkpoint_events = tracing_module.read_trace_events(limit=200, filters={"event_type": "observe_checkpoint"})
        rerun_events = tracing_module.read_trace_events(limit=200, filters={"event_type": "observe_rerun_requested"})
        self.assertTrue(checkpoint_events)
        self.assertTrue(rerun_events)
        self.assertEqual("ckpt-1", (checkpoint_events[-1].get("payload") or {}).get("checkpoint_id"))
        self.assertEqual("ckpt-1", (rerun_events[-1].get("payload") or {}).get("from_checkpoint_id"))

    def test_trace_run_ls_and_open_flow(self):
        run_buf = StringIO()
        with redirect_stdout(run_buf):
            code = observe_main(["trace", "run", "--no-open", "--", "python", "-c", "print('trace-run')"])
        self.assertEqual(0, code)
        run_output = run_buf.getvalue()
        self.assertIn("run_id=", run_output)
        run_id = ""
        for line in run_output.splitlines():
            if line.startswith("run_id="):
                run_id = line.split()[0].split("=", 1)[1].strip()
                break
        self.assertTrue(run_id)

        ls_buf = StringIO()
        with redirect_stdout(ls_buf):
            ls_code = observe_main(["trace", "ls", "--limit", "5"])
        self.assertEqual(0, ls_code)
        self.assertIn(run_id, ls_buf.getvalue())

        open_buf = StringIO()
        with redirect_stdout(open_buf):
            open_code = observe_main(["trace", "open", run_id, "--ui-url", "http://localhost:5173"])
        self.assertEqual(0, open_code)
        self.assertIn("opened=http://localhost:5173", open_buf.getvalue())

        show_buf = StringIO()
        original_log_path = tracing_module.TRACE_LOG_PATH
        tracing_module.TRACE_LOG_PATH = str(Path(tempfile.gettempdir()) / f"hive-observe-mismatch-{uuid.uuid4().hex}.log")
        try:
            with redirect_stdout(show_buf):
                show_code = observe_main(["trace", "show", run_id, "--limit", "200"])
        finally:
            tracing_module.TRACE_LOG_PATH = original_log_path
        self.assertEqual(0, show_code)
        self.assertIn(run_id, show_buf.getvalue())

        show_json_buf = StringIO()
        with redirect_stdout(show_json_buf):
            show_json_code = observe_main(["trace", "show", run_id, "--limit", "200", "--json"])
        self.assertEqual(0, show_json_code)
        self.assertIn(f'"run_id":"{run_id}"', show_json_buf.getvalue())

    def test_trace_run_open_flag_behavior(self):
        default_buf = StringIO()
        with redirect_stdout(default_buf):
            default_code = observe_main(["trace", "run", "--", "python", "-c", "print('open-default')"])
        self.assertEqual(0, default_code)
        self.assertNotIn("ui=", default_buf.getvalue())

        explicit_buf = StringIO()
        with redirect_stdout(explicit_buf):
            explicit_code = observe_main(
                ["trace", "run", "--open", "--ui-url", "http://localhost:5173", "--", "python", "-c", "print('open-explicit')"]
            )
        self.assertEqual(0, explicit_code)
        self.assertIn("ui=http://localhost:5173", explicit_buf.getvalue())

        config_module.TRACE_OPEN_ON_RUN = True
        env_default_buf = StringIO()
        with redirect_stdout(env_default_buf):
            env_default_code = observe_main(["trace", "run", "--", "python", "-c", "print('open-env-default')"])
        self.assertEqual(0, env_default_code)
        self.assertIn("ui=http://localhost:5173", env_default_buf.getvalue())

        override_buf = StringIO()
        with redirect_stdout(override_buf):
            override_code = observe_main(["trace", "run", "--no-open", "--", "python", "-c", "print('open-no-override')"])
        self.assertEqual(0, override_code)
        self.assertNotIn("ui=", override_buf.getvalue())

    def test_trace_run_open_falls_back_to_local_status_page_when_ui_unreachable(self):
        os.environ.pop("HIVE_TRACE_DISABLE_BROWSER", None)
        captured = {"url": "", "html": ""}

        def _fake_browser_open(url, new=2):
            captured["url"] = str(url)
            with urllib.request.urlopen(str(url), timeout=2) as resp:
                captured["html"] = resp.read().decode("utf-8")
            return True

        with mock.patch("hiveos.observe._check_ui_reachable", return_value=(False, "ui unreachable")):
            with mock.patch("hiveos.observe.webbrowser.open", side_effect=_fake_browser_open):
                buf = StringIO()
                with redirect_stdout(buf):
                    code = observe_main(
                        [
                            "trace",
                            "run",
                            "--open",
                            "--ui-url",
                            "http://localhost:5173",
                            "--",
                            "python",
                            "-c",
                            "print('fallback-open')",
                        ]
                    )
        self.assertEqual(0, code)
        self.assertTrue(captured["url"].startswith("http://127.0.0.1:"))
        self.assertIn("Hive Trace Status", captured["html"])
        self.assertIn("http://localhost:5173", captured["html"])

    def test_trace_diff_json_reports_divergence(self):
        run_ids = []
        for text in ("alpha", "beta"):
            buf = StringIO()
            with redirect_stdout(buf):
                code = observe_main(["trace", "run", "--no-open", "--", "python", "-c", f"print('{text}')"])
            self.assertEqual(0, code)
            rid = ""
            for line in buf.getvalue().splitlines():
                if line.startswith("run_id="):
                    rid = line.split()[0].split("=", 1)[1].strip()
                    break
            self.assertTrue(rid)
            run_ids.append(rid)

        diff_buf = StringIO()
        with redirect_stdout(diff_buf):
            diff_code = observe_main(
                ["trace", "diff", run_ids[0], run_ids[1], "--json", "--event-limit", "500", "--tail-lines", "10"]
            )
        self.assertEqual(0, diff_code)
        payload = json.loads(diff_buf.getvalue().strip())
        self.assertEqual(run_ids[0], payload["run_a"]["run_id"])
        self.assertEqual(run_ids[1], payload["run_b"]["run_id"])
        self.assertIn("execution", payload)
        self.assertGreaterEqual(int(payload["execution"]["first_divergence_index"]), 0)

    def test_trace_summary_json_for_run(self):
        run_buf = StringIO()
        with redirect_stdout(run_buf):
            code = observe_main(
                [
                    "trace",
                    "run",
                    "--no-open",
                    "--",
                    "python",
                    "-c",
                    "import sys; print('summary-stdout'); sys.stderr.write('summary-stderr\\n')",
                ]
            )
        self.assertEqual(0, code)
        run_id = ""
        for line in run_buf.getvalue().splitlines():
            if line.startswith("run_id="):
                run_id = line.split()[0].split("=", 1)[1].strip()
                break
        self.assertTrue(run_id)

        summary_buf = StringIO()
        with redirect_stdout(summary_buf):
            summary_code = observe_main(["trace", "summary", run_id, "--json", "--event-limit", "500", "--tail-lines", "5"])
        self.assertEqual(0, summary_code)
        payload = json.loads(summary_buf.getvalue().strip())
        self.assertEqual(run_id, payload["run_id"])
        self.assertEqual("success", payload["status"])
        self.assertIn("summary-stderr", "\n".join(payload.get("stderr_tail") or []))
        self.assertIn("summary-stdout", "\n".join(payload.get("stdout_tail") or []))
        self.assertGreaterEqual(int(payload.get("event_count") or 0), 1)
        self.assertGreaterEqual(len(payload.get("suggestions") or []), 1)

    def test_trace_summary_missing_run_errors(self):
        with self.assertRaises(SystemExit) as ctx:
            observe_main(["trace", "summary", "observe-run:missing-id"])
        self.assertIn("run_id not found", str(ctx.exception))

    def test_trace_tail_json_with_run_filter(self):
        run_ids = []
        for label in ("tail-a", "tail-b"):
            run_buf = StringIO()
            with redirect_stdout(run_buf):
                code = observe_main(["trace", "run", "--no-open", "--", "python", "-c", f"print('{label}')"])
            self.assertEqual(0, code)
            rid = ""
            for line in run_buf.getvalue().splitlines():
                if line.startswith("run_id="):
                    rid = line.split()[0].split("=", 1)[1].strip()
                    break
            self.assertTrue(rid)
            run_ids.append(rid)

        tail_buf = StringIO()
        with redirect_stdout(tail_buf):
            tail_code = observe_main(["trace", "tail", "--run-id", run_ids[0], "--limit", "200", "--json"])
        self.assertEqual(0, tail_code)
        lines = [line.strip() for line in tail_buf.getvalue().splitlines() if line.strip()]
        self.assertGreaterEqual(len(lines), 1)
        for line in lines:
            payload = json.loads(line)
            self.assertEqual(run_ids[0], payload.get("run_id"))

    def test_trace_tail_follow_exits_cleanly_on_keyboard_interrupt(self):
        run_buf = StringIO()
        with redirect_stdout(run_buf):
            code = observe_main(["trace", "run", "--no-open", "--", "python", "-c", "print('tail-follow')"])
        self.assertEqual(0, code)
        run_id = ""
        for line in run_buf.getvalue().splitlines():
            if line.startswith("run_id="):
                run_id = line.split()[0].split("=", 1)[1].strip()
                break
        self.assertTrue(run_id)

        tail_buf = StringIO()
        with mock.patch("hiveos.observe.time.sleep", side_effect=KeyboardInterrupt):
            with redirect_stdout(tail_buf):
                tail_code = observe_main(["trace", "tail", "--run-id", run_id, "--follow", "--limit", "20"])
        self.assertEqual(0, tail_code)
        self.assertIn(run_id, tail_buf.getvalue())

    def test_trace_export_import_round_trip_with_as_run_id(self):
        run_buf = StringIO()
        with redirect_stdout(run_buf):
            code = observe_main(
                [
                    "trace",
                    "run",
                    "--no-open",
                    "--",
                    "python",
                    "-c",
                    "import sys; print('bundle-stdout'); sys.stderr.write('bundle-stderr\\n')",
                ]
            )
        self.assertEqual(0, code)
        source_run_id = ""
        for line in run_buf.getvalue().splitlines():
            if line.startswith("run_id="):
                source_run_id = line.split()[0].split("=", 1)[1].strip()
                break
        self.assertTrue(source_run_id)

        bundle_path = Path(self._trace_home) / "tmp" / "trace.bundle.json"
        export_buf = StringIO()
        with redirect_stdout(export_buf):
            export_code = observe_main(["trace", "export", source_run_id, "--bundle", str(bundle_path)])
        self.assertEqual(0, export_code)
        self.assertTrue(bundle_path.exists())
        self.assertIn("exported=", export_buf.getvalue())

        imported_run_id = "observe-run:imported-bundle"
        import_buf = StringIO()
        with redirect_stdout(import_buf):
            import_code = observe_main(
                ["trace", "import", "--bundle", str(bundle_path), "--as-run-id", imported_run_id]
            )
        self.assertEqual(0, import_code)
        self.assertIn(f"imported={imported_run_id}", import_buf.getvalue())

        record_path = Path(self._trace_home) / "runs" / f"{imported_run_id.replace(':', '_')}.json"
        self.assertTrue(record_path.exists())
        record = json.loads(record_path.read_text(encoding="utf-8"))
        self.assertEqual(imported_run_id, record.get("run_id"))
        self.assertEqual(source_run_id, record.get("import_source_run_id"))
        self.assertTrue(bool(record.get("imported_from_bundle")))

        show_buf = StringIO()
        with redirect_stdout(show_buf):
            show_code = observe_main(["trace", "show", imported_run_id, "--json", "--limit", "300"])
        self.assertEqual(0, show_code)
        lines = [line.strip() for line in show_buf.getvalue().splitlines() if line.strip()]
        self.assertGreaterEqual(len(lines), 1)
        payload = json.loads(lines[-1])
        self.assertEqual(imported_run_id, payload.get("run_id"))

    def test_trace_import_fails_on_existing_run_id_collision(self):
        run_buf = StringIO()
        with redirect_stdout(run_buf):
            code = observe_main(["trace", "run", "--no-open", "--", "python", "-c", "print('collision-source')"])
        self.assertEqual(0, code)
        run_id = ""
        for line in run_buf.getvalue().splitlines():
            if line.startswith("run_id="):
                run_id = line.split()[0].split("=", 1)[1].strip()
                break
        self.assertTrue(run_id)

        bundle_path = Path(self._trace_home) / "tmp" / "collision.bundle.json"
        observe_main(["trace", "export", run_id, "--bundle", str(bundle_path)])
        self.assertTrue(bundle_path.exists())

        with self.assertRaises(SystemExit) as ctx:
            observe_main(["trace", "import", "--bundle", str(bundle_path)])
        self.assertIn("run_id already exists", str(ctx.exception))

    def test_trace_replay_creates_lineage_link(self):
        run_buf = StringIO()
        with redirect_stdout(run_buf):
            code = observe_main(["trace", "run", "--no-open", "--", "python", "-c", "print('replay source text')"])
        self.assertEqual(0, code)
        source_run_id = ""
        for line in run_buf.getvalue().splitlines():
            if line.startswith("run_id="):
                source_run_id = line.split()[0].split("=", 1)[1].strip()
                break
        self.assertTrue(source_run_id)

        replay_buf = StringIO()
        with redirect_stdout(replay_buf):
            replay_code = observe_main(["trace", "replay", source_run_id, "--no-open"])
        self.assertEqual(0, replay_code)
        replay_run_id = ""
        for line in replay_buf.getvalue().splitlines():
            if line.startswith("run_id="):
                replay_run_id = line.split()[0].split("=", 1)[1].strip()
                break
        self.assertTrue(replay_run_id)

        replay_record_path = Path(self._trace_home) / "runs" / f"{replay_run_id.replace(':', '_')}.json"
        replay_record = json.loads(replay_record_path.read_text(encoding="utf-8"))
        self.assertEqual(source_run_id, replay_record.get("replay_of_run_id"))
        self.assertEqual("replay", replay_record.get("source"))
        self.assertIsInstance(replay_record.get("argv"), list)
        self.assertEqual("python", str((replay_record.get("argv") or [""])[0]))
        self.assertEqual("success", replay_record.get("status"))

    def test_trace_diagnose_json_for_failed_run(self):
        run_buf = StringIO()
        with redirect_stdout(run_buf):
            code = observe_main(
                [
                    "trace",
                    "run",
                    "--no-open",
                    "--",
                    "python",
                    "-c",
                    "import sys; sys.stderr.write('boom\\n'); raise SystemExit(3)",
                ]
            )
        self.assertEqual(3, code)
        run_id = ""
        for line in run_buf.getvalue().splitlines():
            if line.startswith("run_id="):
                run_id = line.split()[0].split("=", 1)[1].strip()
                break
        self.assertTrue(run_id)

        diag_buf = StringIO()
        with redirect_stdout(diag_buf):
            diag_code = observe_main(["trace", "diagnose", run_id, "--json"])
        self.assertEqual(0, diag_code)
        payload = json.loads(diag_buf.getvalue().strip())
        self.assertEqual(run_id, payload["run_id"])
        self.assertEqual("failed", payload["status"])
        self.assertIn(payload["category"], {"nonzero_exit", "stderr_heavy"})
        self.assertGreaterEqual(len(payload.get("recommendations") or []), 1)

    def test_trace_diagnose_json_for_success_run(self):
        run_buf = StringIO()
        with redirect_stdout(run_buf):
            code = observe_main(["trace", "run", "--no-open", "--", "python", "-c", "print('healthy')"])
        self.assertEqual(0, code)
        run_id = ""
        for line in run_buf.getvalue().splitlines():
            if line.startswith("run_id="):
                run_id = line.split()[0].split("=", 1)[1].strip()
                break
        self.assertTrue(run_id)

        diag_buf = StringIO()
        with redirect_stdout(diag_buf):
            diag_code = observe_main(["trace", "diagnose", run_id, "--json"])
        self.assertEqual(0, diag_code)
        payload = json.loads(diag_buf.getvalue().strip())
        self.assertEqual("success", payload["status"])
        self.assertEqual("success", payload["category"])

    def test_trace_reconcile_marks_stale_running_run(self):
        run_buf = StringIO()
        with redirect_stdout(run_buf):
            code = observe_main(["trace", "run", "--no-open", "--", "python", "-c", "print('reconcile-source')"])
        self.assertEqual(0, code)
        run_id = ""
        for line in run_buf.getvalue().splitlines():
            if line.startswith("run_id="):
                run_id = line.split()[0].split("=", 1)[1].strip()
                break
        self.assertTrue(run_id)

        run_file = Path(self._trace_home) / "runs" / f"{run_id.replace(':', '_')}.json"
        record = json.loads(run_file.read_text(encoding="utf-8"))
        old_ms = int(record.get("started_at_ms") or 0) - (10 * 60 * 1000)
        record["status"] = "running"
        record["exit_code"] = None
        record["finished_at_ms"] = None
        record["duration_ms"] = None
        record["last_update_ms"] = old_ms
        run_file.write_text(json.dumps(record), encoding="utf-8")

        reconcile_buf = StringIO()
        with redirect_stdout(reconcile_buf):
            reconcile_code = observe_main(["trace", "reconcile", "--stale-after", "1s", "--json"])
        self.assertEqual(0, reconcile_code)
        payload = json.loads(reconcile_buf.getvalue().strip())
        self.assertGreaterEqual(int(payload.get("reconciled_runs") or 0), 1)
        self.assertIn(run_id, list(payload.get("reconciled_run_ids") or []))

        refreshed = json.loads(run_file.read_text(encoding="utf-8"))
        self.assertEqual("failed", refreshed.get("status"))
        self.assertTrue(bool(refreshed.get("stale_reconciled")))

    def test_trace_ls_auto_reconciles_stale_running_run(self):
        run_buf = StringIO()
        with redirect_stdout(run_buf):
            code = observe_main(["trace", "run", "--no-open", "--", "python", "-c", "print('auto-reconcile-source')"])
        self.assertEqual(0, code)
        run_id = ""
        for line in run_buf.getvalue().splitlines():
            if line.startswith("run_id="):
                run_id = line.split()[0].split("=", 1)[1].strip()
                break
        self.assertTrue(run_id)

        run_file = Path(self._trace_home) / "runs" / f"{run_id.replace(':', '_')}.json"
        record = json.loads(run_file.read_text(encoding="utf-8"))
        old_ms = int(record.get("started_at_ms") or 0) - (10 * 60 * 1000)
        record["status"] = "running"
        record["exit_code"] = None
        record["finished_at_ms"] = None
        record["duration_ms"] = None
        record["last_update_ms"] = old_ms
        run_file.write_text(json.dumps(record), encoding="utf-8")
        config_module.TRACE_RECONCILE_STALE_SEC = 1

        ls_buf = StringIO()
        with redirect_stdout(ls_buf):
            ls_code = observe_main(["trace", "ls", "--status", "failed", "--limit", "20"])
        self.assertEqual(0, ls_code)
        self.assertIn(run_id, ls_buf.getvalue())

    def test_trace_archive_unarchive_and_status_filter(self):
        run_buf = StringIO()
        with redirect_stdout(run_buf):
            code = observe_main(["trace", "run", "--no-open", "--", "python", "-c", "print('archive-test')"])
        self.assertEqual(0, code)
        run_id = ""
        for line in run_buf.getvalue().splitlines():
            if line.startswith("run_id="):
                run_id = line.split()[0].split("=", 1)[1].strip()
                break
        self.assertTrue(run_id)

        archive_buf = StringIO()
        with redirect_stdout(archive_buf):
            archive_code = observe_main(["trace", "archive", run_id, "--reason", "unit-test"])
        self.assertEqual(0, archive_code)
        self.assertIn(f"archived={run_id}", archive_buf.getvalue())

        ls_archived = StringIO()
        with redirect_stdout(ls_archived):
            ls_archived_code = observe_main(["trace", "ls", "--status", "archived", "--limit", "20"])
        self.assertEqual(0, ls_archived_code)
        self.assertIn(run_id, ls_archived.getvalue())

        ls_active = StringIO()
        with redirect_stdout(ls_active):
            ls_active_code = observe_main(["trace", "ls", "--status", "active", "--limit", "20"])
        self.assertEqual(0, ls_active_code)
        self.assertNotIn(run_id, ls_active.getvalue())

        unarchive_buf = StringIO()
        with redirect_stdout(unarchive_buf):
            unarchive_code = observe_main(["trace", "unarchive", run_id])
        self.assertEqual(0, unarchive_code)
        self.assertIn(f"unarchived={run_id}", unarchive_buf.getvalue())

    def test_trace_prune_removes_old_archived_runs(self):
        run_buf = StringIO()
        with redirect_stdout(run_buf):
            code = observe_main(["trace", "run", "--no-open", "--", "python", "-c", "print('prune-test')"])
        self.assertEqual(0, code)
        run_id = ""
        for line in run_buf.getvalue().splitlines():
            if line.startswith("run_id="):
                run_id = line.split()[0].split("=", 1)[1].strip()
                break
        self.assertTrue(run_id)

        archive_buf = StringIO()
        with redirect_stdout(archive_buf):
            observe_main(["trace", "archive", run_id, "--reason", "prune-window"])
        run_file = Path(self._trace_home) / "runs" / f"{run_id.replace(':', '_')}.json"
        record = json.loads(run_file.read_text(encoding="utf-8"))
        old_ms = int(record.get("started_at_ms") or 0) - (3 * 24 * 60 * 60 * 1000)
        record["started_at_ms"] = old_ms
        record["finished_at_ms"] = old_ms
        record["archived_at_ms"] = old_ms
        run_file.write_text(json.dumps(record), encoding="utf-8")

        prune_buf = StringIO()
        with redirect_stdout(prune_buf):
            prune_code = observe_main(["trace", "prune", "--older-than", "1d"])
        self.assertEqual(0, prune_code)
        self.assertIn("pruned_runs=", prune_buf.getvalue())
        self.assertFalse(run_file.exists())

    def test_quickstart_command(self):
        buf = StringIO()
        with redirect_stdout(buf):
            code = observe_main(["quickstart", "--no-open"])
        self.assertEqual(0, code)
        text = buf.getvalue()
        self.assertIn("quickstart_complete=true", text)
        self.assertIn("run_id=observe-run:", text)
        self.assertIn("next=hive trace ls", text)

    def test_doctor_command(self):
        buf = StringIO()
        with redirect_stdout(buf):
            code = observe_main(["doctor"])
        self.assertIn(code, (0, 2))
        text = buf.getvalue()
        self.assertIn("[", text)
        self.assertIn("python", text)
        self.assertIn("trace_home", text)

    def test_version_flag(self):
        buf = StringIO()
        with self.assertRaises(SystemExit) as ctx:
            with redirect_stdout(buf):
                observe_main(["--version"])
        self.assertEqual(0, int(ctx.exception.code))
        self.assertRegex(buf.getvalue().strip(), r"^hive\s+\S+$")

    def test_proxy_passthrough_emits_proxy_events(self):
        class UpstreamHandler(BaseHTTPRequestHandler):
            def do_POST(self):  # noqa: N802
                length = int(self.headers.get("Content-Length") or 0)
                body = self.rfile.read(length) if length > 0 else b""
                payload = json.dumps({"ok": True, "size": len(body)}).encode("utf-8")
                self.send_response(200)
                self.send_header("Content-Type", "application/json")
                self.send_header("Content-Length", str(len(payload)))
                self.end_headers()
                self.wfile.write(payload)

            def log_message(self, fmt, *args):  # noqa: A003
                return

        upstream = ThreadingHTTPServer(("127.0.0.1", 0), UpstreamHandler)
        thread = threading.Thread(target=upstream.serve_forever, daemon=True)
        thread.start()
        host, port = upstream.server_address
        upstream_base = f"http://{host}:{port}/v1"
        run_id = f"observe-run:{uuid.uuid4().hex[:8]}"
        try:
            with observe_proxy(run_id=run_id, upstream_base=upstream_base) as proxy:
                body = json.dumps({"hello": "proxy"}).encode("utf-8")
                req = urllib.request.Request(
                    url=f"{proxy.url}/chat/completions",
                    data=body,
                    headers={"Content-Type": "application/json"},
                    method="POST",
                )
                with urllib.request.urlopen(req, timeout=5) as resp:
                    data = json.loads(resp.read().decode("utf-8"))
                    self.assertTrue(data.get("ok"))
        finally:
            upstream.shutdown()
            upstream.server_close()

        req_events = tracing_module.read_trace_events(
            limit=200,
            filters={"run_id": run_id, "event_type": "observe_proxy_request"},
        )
        res_events = tracing_module.read_trace_events(
            limit=200,
            filters={"run_id": run_id, "event_type": "observe_proxy_response"},
        )
        self.assertTrue(req_events)
        self.assertTrue(res_events)

    def test_trace_run_proxy_mode_prints_proxy_url(self):
        class UpstreamHandler(BaseHTTPRequestHandler):
            def do_POST(self):  # noqa: N802
                payload = json.dumps({"ok": True}).encode("utf-8")
                self.send_response(200)
                self.send_header("Content-Type", "application/json")
                self.send_header("Content-Length", str(len(payload)))
                self.end_headers()
                self.wfile.write(payload)

            def log_message(self, fmt, *args):  # noqa: A003
                return

        upstream = ThreadingHTTPServer(("127.0.0.1", 0), UpstreamHandler)
        thread = threading.Thread(target=upstream.serve_forever, daemon=True)
        thread.start()
        host, port = upstream.server_address
        upstream_base = f"http://{host}:{port}/v1"
        try:
            buf = StringIO()
            with redirect_stdout(buf):
                code = observe_main(
                    [
                        "trace",
                        "run",
                        "--no-open",
                        "--proxy",
                        "--proxy-upstream",
                        upstream_base,
                        "--",
                        "python",
                        "-c",
                        "import os; print('base_set', bool(os.getenv('OPENAI_BASE_URL')))",
                    ]
                )
            self.assertEqual(0, code)
            out = buf.getvalue()
            self.assertIn("proxy=http://127.0.0.1:", out)
            self.assertIn("base_set True", out)
        finally:
            upstream.shutdown()
            upstream.server_close()


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