"""
Sprint 10 Tests: server.py split, cancel endpoint, cron history, tool card polish.
"""
import json, pathlib, urllib.error, urllib.request, urllib.parse
from io import BytesIO

from tests.conftest import requires_agent_modules

REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve()

from tests._pytest_port import BASE

def get(path):
    with urllib.request.urlopen(BASE + path, timeout=10) as r:
        return json.loads(r.read()), r.status

def get_text(path):
    with urllib.request.urlopen(BASE + path, timeout=10) as r:
        return r.read().decode(), r.status

def post(path, body=None):
    data = json.dumps(body or {}).encode()
    req = urllib.request.Request(BASE + path, data=data,
                                  headers={"Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=10) as r:
            return json.loads(r.read()), r.status
    except urllib.error.HTTPError as e:
        return json.loads(e.read()), e.code


class CaptureHandler:
    headers = {}

    def __init__(self):
        self.status = None
        self.response_headers = []
        self.wfile = BytesIO()

    def send_response(self, status):
        self.status = status

    def send_header(self, name, value):
        self.response_headers.append((name, value))

    def end_headers(self):
        pass


def make_session(created_list):
    d, _ = post("/api/session/new", {})
    sid = d["session"]["session_id"]
    created_list.append(sid)
    return sid

# ── server.py split: api/ modules served / importable ─────────────────────

def test_health_still_works(cleanup_test_sessions):
    data, status = get("/health")
    assert status == 200
    assert data["status"] == "ok"
    assert "uptime_seconds" in data
    assert "active_streams" in data

def test_api_modules_exist(cleanup_test_sessions):
    """All api/ module files must exist on disk."""
    base = REPO_ROOT / "api"
    for mod in ["__init__.py", "config.py", "helpers.py", "models.py",
                "workspace.py", "upload.py", "streaming.py"]:
        assert (base / mod).exists(), f"Missing api/{mod}"

def test_server_py_under_750_lines(cleanup_test_sessions):
    """server.py should be under 750 lines after the split."""
    lines = len((REPO_ROOT / "server.py").read_text().splitlines())
    assert lines < 750, f"server.py is {lines} lines -- split may not have landed"

def test_api_config_has_cancel_flags(cleanup_test_sessions):
    src = (REPO_ROOT / "api/config.py").read_text()
    assert "CANCEL_FLAGS" in src
    assert "STREAMS" in src

def test_session_crud_still_works(cleanup_test_sessions):
    """Full session lifecycle works after split."""
    created = []
    sid = make_session(created)
    data, status = get(f"/api/session?session_id={urllib.parse.quote(sid)}")
    assert status == 200
    assert data["session"]["session_id"] == sid
    post("/api/session/delete", {"session_id": sid})

def test_static_files_still_served(cleanup_test_sessions):
    for f in ["ui.js", "workspace.js", "sessions.js", "messages.js", "panels.js", "boot.js"]:
        src, status = get_text(f"/static/{f}")
        assert status == 200, f"/static/{f} returned {status}"
        assert len(src) > 100

# ── Cancel endpoint ────────────────────────────────────────────────────────

def test_cancel_requires_stream_id(cleanup_test_sessions):
    try:
        data, status = get("/api/chat/cancel")
        assert status == 400
    except urllib.error.HTTPError as e:
        assert e.code == 400

def test_cancel_nonexistent_stream(cleanup_test_sessions):
    data, status = get("/api/chat/cancel?stream_id=nonexistent_xyz")
    assert status == 200
    assert data["ok"] is True
    assert data["cancelled"] is False

def test_send_button_in_html(cleanup_test_sessions):
    src, _ = get_text("/")
    assert "btnSend" in src                   # single primary action button present
    assert 'id="btnCancel"' not in src        # deprecated composer cancel button removed

def test_cancel_function_in_boot_js(cleanup_test_sessions):
    src, _ = get_text("/static/boot.js")
    assert "async function cancelStream(" in src
    assert "api/chat/cancel" in src

# ── Cron history ───────────────────────────────────────────────────────────

def test_crons_output_limit_param(cleanup_test_sessions):
    """Server accepts limit parameter > 1."""
    data, status = get("/api/crons/output?job_id=nonexistent&limit=20")
    # 404 or 200 with empty -- both acceptable for nonexistent job
    assert status in (200, 404)


@requires_agent_modules
def test_crons_output_rejects_traversal_job_id(monkeypatch, tmp_path):
    """Cron output listing must not read markdown files outside OUTPUT_DIR."""
    from api.routes import _handle_cron_output
    import cron.jobs

    output_dir = tmp_path / "cron" / "output"
    secret_dir = tmp_path / "memories"
    output_dir.mkdir(parents=True)
    secret_dir.mkdir()
    (secret_dir / "MEMORY.md").write_text("SECRET_MARKDOWN_TOKEN\n", encoding="utf-8")
    monkeypatch.setattr(cron.jobs, "OUTPUT_DIR", output_dir)

    handler = CaptureHandler()
    parsed = urllib.parse.urlparse("/api/crons/output?job_id=../../memories&limit=5")
    _handle_cron_output(handler, parsed)

    body = json.loads(handler.wfile.getvalue().decode("utf-8"))
    assert handler.status == 400
    assert body == {"error": "invalid job_id"}
    assert "SECRET_MARKDOWN_TOKEN" not in handler.wfile.getvalue().decode("utf-8")


@requires_agent_modules
def test_crons_output_still_returns_valid_job_outputs(monkeypatch, tmp_path):
    """Valid job ids still list recent markdown output content."""
    from api.routes import _handle_cron_output
    import cron.jobs

    output_dir = tmp_path / "cron" / "output"
    job_dir = output_dir / "job_123"
    job_dir.mkdir(parents=True)
    (job_dir / "run.md").write_text("# Cron Job\n\n## Response\nexpected output\n", encoding="utf-8")
    monkeypatch.setattr(cron.jobs, "OUTPUT_DIR", output_dir)

    handler = CaptureHandler()
    parsed = urllib.parse.urlparse("/api/crons/output?job_id=job_123&limit=5")
    _handle_cron_output(handler, parsed)

    body = json.loads(handler.wfile.getvalue().decode("utf-8"))
    assert handler.status == 200
    assert body["job_id"] == "job_123"
    assert body["outputs"] == [{"filename": "run.md", "content": "# Cron Job\n\n## Response\nexpected output\n"}]

def test_cron_history_button_in_panels_js(cleanup_test_sessions):
    src, _ = get_text("/static/panels.js")
    # After the main-view refactor, cron runs load inline into the detail card
    # via _loadCronDetailRuns() instead of a separate "All runs" button.
    assert "_loadCronDetailRuns" in src
    assert "cron_last_output" in src  # i18n key used by the runs card

def test_cron_output_snippet_helper(cleanup_test_sessions):
    src, _ = get_text("/static/panels.js")
    assert "_cronOutputSnippet" in src


def test_cron_output_usage_metadata_parses_optional_fields(cleanup_test_sessions):
    from api.routes import _cron_output_usage_metadata

    content = "\n".join([
        "# Cron Job: Nightly",
        "**Model:** openai-codex/gpt-5.5",
        "**Tokens:** 12,345 in / 678 out",
        "**Estimated cost:** $0.0123 (estimated)",
        "**Duration:** 42.5s",
        "",
        "## Response",
        "Done",
    ])

    usage = _cron_output_usage_metadata(content)

    assert usage["model"] == "openai-codex/gpt-5.5"
    assert usage["input_tokens"] == 12345
    assert usage["output_tokens"] == 678
    assert usage["total_tokens"] == 13023
    assert usage["estimated_cost_usd"] == 0.0123
    assert usage["duration_seconds"] == 42.5


def test_cron_output_usage_strip_render_hook(cleanup_test_sessions):
    src, _ = get_text("/static/panels.js")
    css, _ = get_text("/static/style.css")

    assert "_formatCronRunUsageStrip(run.usage)" in src
    assert "_formatCronRunUsageStrip(data.usage)" in src
    assert "cron-run-usage-strip" in src
    assert ".cron-run-usage-strip" in css


def test_cron_output_window_preserves_response_after_large_prompt(cleanup_test_sessions):
    """Large skill dumps before ## Response must not hide the useful output."""
    from api.routes import _cron_output_content_window

    content = (
        "Job metadata\n"
        "## Prompt\n"
        + ("skill dump\n" * 1200)
        + "user prompt\n"
        "## Response\n"
        "actual useful cron result\n"
    )

    window = _cron_output_content_window(content, limit=8000)

    assert len(window) <= 8000
    assert "## Response" in window
    assert "actual useful cron result" in window
    assert "Job metadata" in window


def test_cron_output_window_without_response_uses_tail(cleanup_test_sessions):
    """Without a response marker, keep the newest tail rather than old prompt text."""
    from api.routes import _cron_output_content_window

    content = "old prompt\n" + ("x" * 9000) + "tail result"

    window = _cron_output_content_window(content, limit=8000)

    assert len(window) == 8000
    assert window.endswith("tail result")
    assert "old prompt" not in window

# ── Tool card polish ───────────────────────────────────────────────────────

def test_tool_card_running_dot_in_css(cleanup_test_sessions):
    src, _ = get_text("/static/style.css")
    assert "tool-card-running-dot" in src

def test_tool_card_show_more_in_ui_js(cleanup_test_sessions):
    src, _ = get_text("/static/ui.js")
    assert "Show more" in src
    assert "tool-card-more" in src

def test_tool_card_smart_truncation_in_ui_js(cleanup_test_sessions):
    src, _ = get_text("/static/ui.js")
    assert "displaySnippet" in src
    assert "lastBreak" in src

def test_cancel_sse_event_handler_in_messages_js(cleanup_test_sessions):
    src, _ = get_text("/static/messages.js")
    assert "addEventListener('cancel'" in src
    assert "Task cancelled" in src

def test_active_stream_id_tracked(cleanup_test_sessions):
    src, _ = get_text("/static/messages.js")
    assert "S.activeStreamId" in src
