"""The publish cliff: an app with a FastAPI backend works in preview and 404s on
its public URL, because publishing uploads a static bundle and the edge has no
/api/* route. Before this gate, nothing in the publish path noticed."""
import uuid

import pytest

from backend.apps.outputs import publish_common
from backend.apps.outputs.models import Output, PublishReview
from backend.apps.outputs.publish_capability import (
    check_publish_capability,
    merge_capability,
)


@pytest.fixture
def p_ws_root(tmp_path, monkeypatch):
    root = tmp_path / "ws"
    root.mkdir()
    monkeypatch.setattr(publish_common, "OUTPUTS_WORKSPACE_DIR", str(root))
    return root


def p_app(ws_root, *, workspace: bool = True) -> Output:
    wsid = uuid.uuid4().hex if workspace else None
    if wsid:
        (ws_root / wsid).mkdir()
    return Output(
        name="Demo", description="", icon="view_quilt",
        input_schema={"type": "object", "properties": {}, "required": []},
        files={}, workspace_id=wsid, session_id=None,
    )


def p_seed(ws_root, output, *, env: str, frontend: str = "", backend_main: bool = False):
    root = ws_root / (output.workspace_id or "")
    (root / ".env").write_text(env)
    if frontend:
        fe = root / "frontend" / "src"
        fe.mkdir(parents=True)
        (fe / "api.ts").write_text(frontend)
    if backend_main:
        be = root / "backend"
        be.mkdir()
        (be / "main.py").write_text("app = 1\n")
    return root


def test_flat_app_has_no_capability_problem(p_ws_root):
    out = p_app(p_ws_root, workspace=False)
    assert check_publish_capability(out).findings == []


def test_frontend_only_workspace_is_clean(p_ws_root):
    out = p_app(p_ws_root)
    p_seed(p_ws_root, out, env="BACKEND_PORT=NONE\n", frontend="fetch('/data.json')\n")
    report = check_publish_capability(out)
    assert report.backend_enabled is False
    assert report.findings == []


def test_backend_plus_api_calls_is_blocked(p_ws_root):
    out = p_app(p_ws_root)
    p_seed(
        p_ws_root, out,
        env="BACKEND_PORT=8123 # chosen by backend_init.sh\nFRONTEND_PORT=4949\n",
        frontend="export const JOBS = '/api/jobs';\nfetch(JOBS);\n",
        backend_main=True,
    )
    report = check_publish_capability(out)
    assert report.backend_enabled is True
    assert report.backend_port == "8123"
    assert report.api_callers == ["frontend/src/api.ts"]
    assert len(report.findings) == 2
    joined = " ".join(report.findings)
    assert "frontend/src/api.ts" in joined
    assert "OUTPUT_COMPUTE" in joined


def test_backend_with_no_callers_loses_nothing(p_ws_root):
    """A backend nothing calls is dead weight, not broken functionality."""
    out = p_app(p_ws_root)
    p_seed(
        p_ws_root, out, env="BACKEND_PORT=8123\n",
        frontend="const x = 1;\n", backend_main=True,
    )
    report = check_publish_capability(out)
    assert report.backend_enabled is True
    assert report.findings == []


def test_backend_dir_without_port_still_counts(p_ws_root):
    """backend_init.sh calls this state inconsistent; publishing must not shrug."""
    out = p_app(p_ws_root)
    p_seed(
        p_ws_root, out, env="BACKEND_PORT=NONE\n",
        frontend="fetch('/api/things')\n", backend_main=True,
    )
    assert check_publish_capability(out).findings != []


def test_apiary_is_not_an_api_call(p_ws_root):
    """Prefix matching would flag /apiary and /rapid; the boundary is load-bearing."""
    out = p_app(p_ws_root)
    p_seed(
        p_ws_root, out, env="BACKEND_PORT=8123\n",
        frontend="fetch('/apiary/bees'); fetch('/rapid');\n", backend_main=True,
    )
    assert check_publish_capability(out).findings == []


def test_backend_dir_is_not_scanned_for_callers(p_ws_root):
    """The backend's own source mentioning /api must not count as a frontend caller."""
    out = p_app(p_ws_root)
    root = p_seed(p_ws_root, out, env="BACKEND_PORT=8123\n", backend_main=True)
    (root / "backend" / "routes.js").write_text("// mounts /api/jobs\n")
    assert check_publish_capability(out).findings == []


def test_merge_escalates_a_clean_security_review_to_block(p_ws_root):
    out = p_app(p_ws_root)
    p_seed(
        p_ws_root, out, env="BACKEND_PORT=8123\n",
        frontend="fetch('/api/x')\n", backend_main=True,
    )
    merged = merge_capability(out, PublishReview(verdict="clean", findings=[]))
    assert merged.verdict == "block"
    assert len(merged.findings) == 2


def test_merge_preserves_security_findings_and_order(p_ws_root):
    out = p_app(p_ws_root)
    p_seed(
        p_ws_root, out, env="BACKEND_PORT=8123\n",
        frontend="fetch('/api/x')\n", backend_main=True,
    )
    merged = merge_capability(
        out, PublishReview(verdict="warn", findings=["reads os.environ"], scanned_files=["a.py"]),
    )
    assert merged.findings[-1] == "reads os.environ"
    assert merged.scanned_files == ["a.py"]


def test_merge_is_a_passthrough_when_nothing_is_lost(p_ws_root):
    out = p_app(p_ws_root)
    p_seed(p_ws_root, out, env="BACKEND_PORT=NONE\n")
    review = PublishReview(verdict="warn", findings=["something else"])
    assert merge_capability(out, review) is review


def test_missing_env_file_does_not_explode(p_ws_root):
    """A half-seeded workspace must read as 'no backend', not raise."""
    out = p_app(p_ws_root)
    report = check_publish_capability(out)
    assert report.backend_enabled is False
    assert report.findings == []


@pytest.mark.asyncio
async def test_publish_route_refuses_to_ship_a_broken_app(p_ws_root, monkeypatch):
    """The route is where the loss was silent: it built and uploaded regardless."""
    from backend.apps.outputs import outputs as outputs_mod
    from backend.apps.outputs.models import PublishRequest

    out = p_app(p_ws_root)
    p_seed(
        p_ws_root, out, env="BACKEND_PORT=8123\n",
        frontend="fetch('/api/jobs')\n", backend_main=True,
    )
    built = []
    monkeypatch.setattr(outputs_mod, "load", lambda _: out)
    monkeypatch.setattr(outputs_mod, "load_settings", lambda: None)
    monkeypatch.setattr(outputs_mod, "build_static", lambda o: built.append(o))

    res = await outputs_mod.publish_output(PublishRequest(output_id=out.id))

    assert res["ok"] is False
    assert res["blocked"] is True
    assert res["review"]["verdict"] == "block"
    assert built == [], "publish must not build once the gate has fired"


@pytest.mark.asyncio
async def test_force_is_still_the_escape_hatch(p_ws_root, monkeypatch):
    """A user who read the finding can still ship; the gate informs, it does not trap."""
    from backend.apps.outputs import outputs as outputs_mod
    from backend.apps.outputs.models import PublishRequest

    out = p_app(p_ws_root)
    p_seed(
        p_ws_root, out, env="BACKEND_PORT=8123\n",
        frontend="fetch('/api/jobs')\n", backend_main=True,
    )
    reached = []

    async def p_boom(_):
        reached.append(True)
        raise publish_common.PublishError("stopped past the gate")

    monkeypatch.setattr(outputs_mod, "load", lambda _: out)
    monkeypatch.setattr(outputs_mod, "save", lambda _: None)
    monkeypatch.setattr(outputs_mod, "load_settings", lambda: None)
    monkeypatch.setattr(outputs_mod, "build_static", p_boom)

    res = await outputs_mod.publish_output(PublishRequest(output_id=out.id, force=True))

    assert reached == [True], "force must skip the gate and reach the build"
    assert res["ok"] is False
