"""Tests for `agnes store` and `agnes my-stack` Typer wrappers.

Smoke + happy-path. Network calls are mocked so tests don't depend on a
running server.
"""

from __future__ import annotations

import re

from typer.testing import CliRunner

from cli.commands.my_stack import my_stack_app
from cli.commands.store import store_app

runner = CliRunner()
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")


def _clean(s: str) -> str:
    return _ANSI_RE.sub("", s)


# ---------------------------------------------------------------------------
# Help-text smoke tests — guard against accidental command renames.
# ---------------------------------------------------------------------------


def test_store_help_lists_subcommands():
    r = runner.invoke(store_app, ["--help"])
    assert r.exit_code == 0
    out = _clean(r.output)
    for cmd in ("list", "show", "install", "uninstall", "upload", "update", "delete", "mine"):
        assert cmd in out, f"missing subcommand {cmd!r} in help"


def test_admin_store_help_lists_subcommands():
    from cli.commands.admin_store import admin_store_app
    r = runner.invoke(admin_store_app, ["--help"])
    assert r.exit_code == 0
    out = _clean(r.output)
    for cmd in ("pull", "push", "info"):
        assert cmd in out


def test_my_stack_help_lists_subcommands():
    r = runner.invoke(my_stack_app, ["--help"])
    assert r.exit_code == 0
    out = _clean(r.output)
    for cmd in ("show", "toggle"):
        assert cmd in out


def test_store_list_default_help():
    r = runner.invoke(store_app, ["list", "--help"])
    assert r.exit_code == 0
    out = _clean(r.output)
    for opt in ("--type", "--category", "--search", "--owner", "--limit", "--skip", "--json"):
        assert opt in out


# ---------------------------------------------------------------------------
# Happy-path mocked tests.
# ---------------------------------------------------------------------------


def test_store_list_renders_table(monkeypatch):
    sample = {
        "items": [
            {
                "id": "abc123",
                "type": "skill",
                "name": "code-review",
                "owner_username": "alice",
                "install_count": 5,
                "version": "deadbeef00000000",
            },
        ],
        "total": 1,
        "skip": 0,
        "limit": 24,
    }
    import cli.commands.store as store_mod
    monkeypatch.setattr(store_mod, "api_get_json", lambda *a, **kw: sample)

    r = runner.invoke(store_app, ["list"])
    assert r.exit_code == 0, r.output
    out = _clean(r.output)
    assert "1 entit" in out
    assert "code-review" in out
    assert "alice" in out


def test_store_install_calls_api(monkeypatch):
    captured: dict = {}

    def _post(path, payload):
        captured["path"] = path
        captured["payload"] = payload
        return {"entity_id": "xyz", "installed": True}

    import cli.commands.store as store_mod
    monkeypatch.setattr(store_mod, "api_post_json", _post)

    r = runner.invoke(store_app, ["install", "xyz"])
    assert r.exit_code == 0, r.output
    assert captured["path"] == "/api/store/entities/xyz/install"
    assert "Installed" in _clean(r.output)


def test_store_upload_sends_multipart(monkeypatch, tmp_path):
    captured: dict = {}

    def _multipart(path, *, files, data):
        captured["path"] = path
        captured["data"] = data
        captured["files_keys"] = list(files.keys())
        return {
            "id": "new-id",
            "name": data.get("name", "fallback"),
            "invocation_name": "fallback-by-someone",
            "version": "abcd1234",
        }

    import cli.commands.store as store_mod
    monkeypatch.setattr(store_mod, "api_post_multipart", _multipart)

    zip_path = tmp_path / "skill.zip"
    zip_path.write_bytes(b"PK\x03\x04fake-zip-content")

    r = runner.invoke(
        store_app,
        ["upload", "skill", str(zip_path), "--name", "my-skill", "--description", "d"],
    )
    assert r.exit_code == 0, r.output
    assert captured["path"] == "/api/store/entities"
    assert captured["data"]["type"] == "skill"
    assert captured["data"]["name"] == "my-skill"
    assert captured["data"]["description"] == "d"
    assert captured["files_keys"] == ["file"]


def test_my_stack_show_renders(monkeypatch):
    sample = {
        "curated": [
            {
                "marketplace_id": "official",
                "marketplace_slug": "official",
                "plugin_name": "alpha",
                "manifest_name": "alpha",
                "version": "1.0",
                "enabled": True,
            },
        ],
        "store": [
            {
                "entity_id": "e1",
                "type": "skill",
                "name": "code-review",
                "owner_username": "alice",
                "version": "abcd",
                "invocation_name": "code-review-by-alice",
                "install_count": 1,
            },
        ],
    }
    import cli.commands.my_stack as ms_mod
    monkeypatch.setattr(ms_mod, "api_get_json", lambda *a, **kw: sample)

    r = runner.invoke(my_stack_app, ["show"])
    assert r.exit_code == 0, r.output
    out = _clean(r.output)
    assert "Curated" in out and "alpha" in out
    assert "From Store" in out and "code-review-by-alice" in out


def test_my_stack_toggle_requires_on_or_off():
    r = runner.invoke(my_stack_app, ["toggle", "official", "alpha"])
    assert r.exit_code == 2
    assert "exactly one" in _clean(r.output) or "exactly one" in _clean(r.stderr or "")


def test_my_stack_toggle_writes_put(monkeypatch):
    captured: dict = {}

    def _put(path, payload):
        captured["path"] = path
        captured["payload"] = payload
        return {"ok": True}

    import cli.commands.my_stack as ms_mod
    monkeypatch.setattr(ms_mod, "api_put_json", _put)

    r = runner.invoke(my_stack_app, ["toggle", "official", "alpha", "--off"])
    assert r.exit_code == 0, r.output
    assert captured["path"] == "/api/my-stack/curated/official/alpha"
    assert captured["payload"] == {"enabled": False}
    assert "DISABLED" in _clean(r.output)


# ---------------------------------------------------------------------------
# `agnes store update`
# ---------------------------------------------------------------------------


def test_store_update_help_lists_options():
    r = runner.invoke(store_app, ["update", "--help"])
    assert r.exit_code == 0
    out = _clean(r.output)
    for opt in ("--description", "--category", "--video-url", "--photo", "--zip"):
        assert opt in out


def test_store_update_no_fields_exit_2():
    r = runner.invoke(store_app, ["update", "abc123"])
    assert r.exit_code == 2
    assert "Nothing to update" in _clean(r.output)


def test_store_update_sends_put_multipart(monkeypatch):
    captured: dict = {}

    def _put(path, *, files, data):
        captured["path"] = path
        captured["files"] = files
        captured["data"] = data
        return {"id": "abc", "version": "newhash01234567"}

    import cli.commands.store as store_mod
    monkeypatch.setattr(store_mod, "api_put_multipart", _put)

    r = runner.invoke(store_app, ["update", "abc", "--description", "new desc"])
    assert r.exit_code == 0, r.output
    assert captured["path"] == "/api/store/entities/abc"
    assert captured["data"] == {"description": "new desc"}
    assert captured["files"] is None
    assert "Updated" in _clean(r.output)


# ---------------------------------------------------------------------------
# `agnes store pull` / `agnes store info`
# ---------------------------------------------------------------------------


def test_admin_store_pull_writes_zip(monkeypatch, tmp_path):
    """Bulk pull of all Store entities lives under `agnes admin store pull`."""
    from cli.commands.admin import admin_app
    from cli.commands import admin_store as admin_store_mod

    captured: dict = {}

    def _stream(path, dest, **params):
        captured["path"] = path
        captured["params"] = params
        with open(dest, "wb") as f:
            f.write(b"PK\x03\x04fakezip")
        return 9

    monkeypatch.setattr(admin_store_mod, "api_get_stream", _stream)

    out = tmp_path / "store.zip"
    r = runner.invoke(admin_app, ["store", "pull", "-o", str(out)])
    assert r.exit_code == 0, r.output
    assert captured["path"] == "/api/store/bundle.zip"
    # `mine` uses owner=me; bulk pull does NOT.
    assert "owner" not in captured["params"]
    assert "Wrote 9 bytes" in _clean(r.output)
    assert out.exists()


def test_admin_store_pull_unpack(monkeypatch, tmp_path):
    """`agnes admin store pull --unpack DIR` streams + extracts."""
    import zipfile
    from cli.commands.admin import admin_app
    from cli.commands import admin_store as admin_store_mod

    fake_zip_path = tmp_path / "_fake.zip"
    with zipfile.ZipFile(fake_zip_path, "w") as zf:
        zf.writestr("manifest.json", '{"format":1,"entries":[]}')
        zf.writestr("entities/abc/plugin/.claude-plugin/plugin.json", '{}')

    def _stream(path, dest, **params):
        from pathlib import Path as _P
        with open(dest, "wb") as fh:
            fh.write(_P(fake_zip_path).read_bytes())
        return _P(dest).stat().st_size

    monkeypatch.setattr(admin_store_mod, "api_get_stream", _stream)

    target = tmp_path / "unpacked"
    r = runner.invoke(admin_app, ["store", "pull", "--unpack", str(target)])
    assert r.exit_code == 0, r.output
    assert (target / "manifest.json").is_file()
    assert (target / "entities/abc/plugin/.claude-plugin/plugin.json").is_file()


def test_store_mine_uses_owner_me_param(monkeypatch, tmp_path):
    """`agnes store mine` is the user-facing variant — same endpoint with
    `?owner=me` so server can scope to caller's own entities."""
    captured: dict = {}

    def _stream(path, dest, **params):
        captured["path"] = path
        captured["params"] = params
        with open(dest, "wb") as f:
            f.write(b"PK\x03\x04mine")
        return 7

    import cli.commands.store as store_mod
    monkeypatch.setattr(store_mod, "api_get_stream", _stream)

    out = tmp_path / "mine.zip"
    r = runner.invoke(store_app, ["mine", "-o", str(out)])
    assert r.exit_code == 0, r.output
    assert captured["path"] == "/api/store/bundle.zip"
    assert captured["params"] == {"owner": "me"}
    assert out.exists()


def test_admin_store_info_summarizes(monkeypatch):
    from cli.commands.admin import admin_app
    from cli.commands import admin_store as admin_store_mod

    page1 = {
        "items": [
            {"type": "skill", "file_size": 1024},
            {"type": "skill", "file_size": 512},
            {"type": "agent", "file_size": 256},
        ],
        "total": 3, "skip": 0, "limit": 100,
    }
    empty = {"items": [], "total": 3, "skip": 100, "limit": 100}
    pages = [page1, empty]

    monkeypatch.setattr(admin_store_mod, "api_get_json", lambda *a, **kw: pages.pop(0))

    r = runner.invoke(admin_app, ["store", "info"])
    assert r.exit_code == 0, r.output
    out = _clean(r.output)
    assert "3 entit" in out
    assert "skill" in out and "2" in out
    assert "agent" in out and "1" in out


def test_admin_store_info_json(monkeypatch):
    from cli.commands.admin import admin_app
    from cli.commands import admin_store as admin_store_mod
    one = {
        "items": [{"type": "plugin", "file_size": 999}],
        "total": 1, "skip": 0, "limit": 100,
    }
    pages = [one, {"items": [], "total": 1, "skip": 100, "limit": 100}]
    monkeypatch.setattr(admin_store_mod, "api_get_json", lambda *a, **kw: pages.pop(0))

    r = runner.invoke(admin_app, ["store", "info", "--json"])
    assert r.exit_code == 0, r.output
    import json as _json
    body = _json.loads(_clean(r.output))
    assert body["total_entities"] == 1
    assert body["by_type"] == {"plugin": 1}


# ---------------------------------------------------------------------------
# `agnes admin store push`
# ---------------------------------------------------------------------------


def test_admin_store_push_help():
    from cli.commands.admin_store import admin_store_app
    r = runner.invoke(admin_store_app, ["--help"])
    assert r.exit_code == 0
    assert "push" in _clean(r.output)


def test_admin_store_push_invalid_mode_exit_2(tmp_path):
    """Single-command Typer app — invoke via parent so the `push` token
    actually routes to the subcommand (otherwise Typer collapses the lone
    command and treats `push` as the SOURCE positional)."""
    from cli.commands.admin import admin_app
    bundle = tmp_path / "x.zip"
    bundle.write_bytes(b"PK\x03\x04")
    r = runner.invoke(admin_app, ["store", "push", str(bundle), "--mode", "wat"])
    assert r.exit_code == 2
    assert "merge|replace|skip" in _clean(r.output)


def test_admin_store_push_zips_directory(monkeypatch, tmp_path):
    """When source is a directory, CLI must zip it client-side and POST."""
    import zipfile as _zf

    captured: dict = {}

    def _post(path, *, files, data):
        captured["path"] = path
        captured["data"] = data
        zip_bytes = files["file"][1]
        with _zf.ZipFile(__import__("io").BytesIO(zip_bytes)) as zf:
            captured["names"] = sorted(zf.namelist())
        return {
            "imported": 1, "replaced": 0, "skipped": 0,
            "stub_users_created": 0, "errors": [],
        }

    from cli.commands import admin_store as admin_store_mod
    from cli.commands.admin import admin_app
    monkeypatch.setattr(admin_store_mod, "api_post_multipart", _post)

    bundle_dir = tmp_path / "bundle"
    (bundle_dir / "entities" / "abc" / "plugin").mkdir(parents=True)
    (bundle_dir / "manifest.json").write_text('{"format":1,"entries":[]}')
    (bundle_dir / "entities" / "abc" / "plugin" / "marker.txt").write_text("x")

    r = runner.invoke(
        admin_app, ["store", "push", str(bundle_dir), "--mode", "merge", "--yes"],
    )
    assert r.exit_code == 0, r.output
    assert captured["path"] == "/api/store/import-bundle"
    assert captured["data"] == {"mode": "merge"}
    assert "manifest.json" in captured["names"]
    assert "entities/abc/plugin/marker.txt" in captured["names"]
    assert "imported=1" in _clean(r.output)


def test_admin_store_push_directory_without_manifest_exit_2(tmp_path):
    from cli.commands.admin import admin_app
    empty_dir = tmp_path / "no_manifest"
    empty_dir.mkdir()
    r = runner.invoke(
        admin_app, ["store", "push", str(empty_dir), "--yes"],
    )
    assert r.exit_code == 2
    assert "manifest.json" in _clean(r.output)
