"""The M1 spine: project create, the concurrent-stages rule, approvals, the
404-vs-403 discipline, and the client/agency split on what is rendered."""

import itertools

from tests.conftest import OWNER, as_user


def _code(resp):
    d = resp.json().get("detail")
    return d.get("error_code") if isinstance(d, dict) else None


_n = itertools.count(1)


def _mk(client, name="", template="website_build_v1"):
    """A distinct project per call — the accounts store lives for the whole
    session, so a fixed name collides on the second test."""
    label = name or f"Barnet Site {next(_n)}"
    r = client.post("/api/projects", json={"display_name": label, "template_key": template})
    assert r.status_code == 201, r.text
    return r.json()["id"]


def test_create_makes_the_kit_instance_and_the_row_together(client, kit, agency):
    """If these two ever split, the kit's invite/grant screens have nothing to
    grant on and the whole gate collapses (§5.1)."""
    as_user(client, agency)
    pid = _mk(client, "Barnet Site")
    assert pid == "barnet-site"
    from app import accounts
    assert accounts.instance_obj(pid) is not None          # the kit side exists
    body = client.get(f"/api/projects/{pid}").json()      # ...and so does ours
    assert body["display_name"] == "Barnet Site"
    assert len(body["stages"]) == 7                       # template deep-copied


def test_template_copy_is_a_copy_not_a_link(client, kit, agency, session):
    """Editing or deleting a template must be structurally unable to reach a
    live project (§5.5)."""
    as_user(client, agency)
    pid = _mk(client)
    from app.models import Stage, StageTemplate
    st = session.get(StageTemplate, "onboarding")
    st.title = "RENAMED IN THE TEMPLATE"
    session.commit()
    titles = [s.title for s in session.query(Stage).filter_by(project_id=pid)]
    assert "RENAMED IN THE TEMPLATE" not in titles
    assert "Onboarding" in titles


def test_stages_are_concurrent_and_close_independently(client, kit, agency):
    """No cursor, no auto-advance: several stages run at once and each closes
    on its own (D13)."""
    as_user(client, agency)
    pid = _mk(client)
    stages = client.get(f"/api/projects/{pid}").json()["stages"]
    staging = next(s for s in stages if s["title"] == "Staging")
    reviewing = next(s for s in stages if s["title"] == "Reviewing")

    for s in (staging, reviewing):
        client.post(f"/api/projects/{pid}/stages/{s['id']}/status", json={"status": "active"})
    body = client.get(f"/api/projects/{pid}").json()
    assert sorted(body["active_stages"]) == ["Reviewing", "Staging"]

    # closing one leaves the other exactly where it was
    client.post(f"/api/projects/{pid}/stages/{staging['id']}/status", json={"status": "done"})
    body = client.get(f"/api/projects/{pid}").json()
    assert body["active_stages"] == ["Reviewing"]
    assert next(s for s in body["stages"] if s["id"] == staging["id"])["status"] == "done"


def test_an_invisible_project_is_404_never_403(client, kit, agency):
    as_user(client, agency)
    pid = _mk(client)
    kit.member("stranger", "member")
    as_user(client, "stranger")
    r = client.get(f"/api/projects/{pid}")
    assert r.status_code == 404 and _code(r) == "NOT_FOUND"
    # ...and they cannot compose
    assert client.post("/api/projects", json={"display_name": "Nope"}).status_code == 403


def test_client_sees_only_what_is_client_visible(client, kit, agency, session):
    as_user(client, agency)
    pid = _mk(client)
    from app.models import Stage
    hidden = session.query(Stage).filter_by(project_id=pid, title="Build").one()
    hidden.client_visible = False
    session.commit()

    # a reference section defaults to INTERNAL — this is where a staging
    # password gets pasted at 11pm
    r = client.post(f"/api/projects/{pid}/reference",
                    json={"title": "Server", "body_md": "password: hunter2"})
    assert r.status_code == 201
    assert r.json()["warnings"], "the secret lint should warn (never block)"

    kit.member("cleo_v", "member")
    kit.grant("cleo_v", pid, "member")
    as_user(client, "cleo_v")
    body = client.get(f"/api/projects/{pid}").json()
    assert "Build" not in [s["title"] for s in body["stages"]]
    assert body["reference"] == []
    assert body["is_agency"] is False


def test_approval_is_append_only_and_snapshots_the_name(client, kit, agency):
    as_user(client, agency)
    pid = _mk(client)
    stages = client.get(f"/api/projects/{pid}").json()["stages"]
    mockups = next(s for s in stages if s["title"] == "Mockups")
    assert mockups["requires_approval"] is True

    kit.member("cleo", "member")
    kit.grant("cleo", pid, "member")
    as_user(client, "cleo")

    # not open yet
    r = client.post(f"/api/projects/{pid}/stages/{mockups['id']}/approve")
    assert r.status_code == 409 and _code(r) == "NOT_ACTIVE"

    as_user(client, agency)
    client.post(f"/api/projects/{pid}/stages/{mockups['id']}/status", json={"status": "active"})

    as_user(client, "cleo")
    body = client.get(f"/api/projects/{pid}").json()
    assert any(n["kind"] == "approval" for n in body["needed_from_you"])

    r = client.post(f"/api/projects/{pid}/stages/{mockups['id']}/approve",
                    json={"note": "Looks great"})
    assert r.status_code == 200
    appr = next(s for s in r.json()["project"]["stages"]
                if s["id"] == mockups["id"])["approval"]
    assert appr["by"] == "cleo" and appr["note"] == "Looks great"

    # the name is a SNAPSHOT: removing the account must not rewrite history
    kit.bwa.remove_member(OWNER, "cleo")
    as_user(client, agency)
    still = next(s for s in client.get(f"/api/projects/{pid}").json()["stages"]
                 if s["id"] == mockups["id"])["approval"]
    assert still["by"] == "cleo"
    # ...and it cannot be given twice
    kit.member("cleo2", "member")
    kit.grant("cleo2", pid, "member")
    as_user(client, "cleo2")
    assert client.post(f"/api/projects/{pid}/stages/{mockups['id']}/approve").status_code == 409
    # the strip empties once it is answered
    assert client.get(f"/api/projects/{pid}").json()["needed_from_you"] == []


def test_activity_is_recorded_and_filtered_for_clients(client, kit, agency):
    as_user(client, agency)
    pid = _mk(client)
    verbs = [a["verb"] for a in client.get(f"/api/projects/{pid}/activity").json()["activity"]]
    assert "project.created" in verbs and "project.template_applied" in verbs


# ── reference sections: written internal, published when they're right ──

def test_a_reference_section_is_internal_until_it_is_published(client, kit, agency):
    """`client_visible` defaults FALSE — this is exactly where a staging
    password gets pasted at 11pm."""
    as_user(client, agency)
    pid = client.post("/api/projects", json={"display_name": f"Ref {next(_n)}"}).json()["id"]
    rid = client.post(f"/api/projects/{pid}/reference",
                      json={"title": "Hosting", "body_md": "cPanel, renews in March"}
                      ).json()["id"]

    kit.member("cleo", "member")
    kit.grant("cleo", pid, "member")
    as_user(client, "cleo")
    assert client.get(f"/api/projects/{pid}").json()["reference"] == []

    as_user(client, agency)
    assert client.put(f"/api/projects/{pid}/reference/{rid}",
                      json={"client_visible": True}).status_code == 200
    as_user(client, "cleo")
    assert [r["title"] for r in client.get(f"/api/projects/{pid}").json()["reference"]] == ["Hosting"]


def test_editing_a_reference_section_lints_for_credentials_without_blocking(client, agency):
    as_user(client, agency)
    pid = client.post("/api/projects", json={"display_name": f"Ref {next(_n)}"}).json()["id"]
    rid = client.post(f"/api/projects/{pid}/reference",
                      json={"title": "Notes", "body_md": "nothing here"}).json()["id"]
    r = client.put(f"/api/projects/{pid}/reference/{rid}",
                   json={"body_md": "wp-admin password: hunter2"})
    assert r.status_code == 200
    assert r.json()["warnings"]                       # warned…
    body = client.get(f"/api/projects/{pid}").json()["reference"][0]["body_md"]
    assert "hunter2" in body                          # …and never blocked


def test_a_client_cannot_edit_or_delete_reference(client, kit, agency):
    as_user(client, agency)
    pid = client.post("/api/projects", json={"display_name": f"Ref {next(_n)}"}).json()["id"]
    rid = client.post(f"/api/projects/{pid}/reference",
                      json={"title": "Hosting", "body_md": "x", "client_visible": True}
                      ).json()["id"]
    kit.member("cleo", "member")
    kit.grant("cleo", pid, "member")
    as_user(client, "cleo")
    assert client.put(f"/api/projects/{pid}/reference/{rid}", json={"title": "x"}).status_code == 403
    assert client.delete(f"/api/projects/{pid}/reference/{rid}").status_code == 403


def test_delete_removes_the_section(client, agency):
    as_user(client, agency)
    pid = client.post("/api/projects", json={"display_name": f"Ref {next(_n)}"}).json()["id"]
    rid = client.post(f"/api/projects/{pid}/reference",
                      json={"title": "Scratch", "body_md": "x"}).json()["id"]
    assert client.delete(f"/api/projects/{pid}/reference/{rid}").status_code == 200
    assert client.get(f"/api/projects/{pid}").json()["reference"] == []
    assert client.delete(f"/api/projects/{pid}/reference/{rid}").status_code == 404


def test_a_choice_fields_options_travel_with_its_value(client, agency):
    """The dictionary's promise: label AND shape are the same on every
    project, so the editor never has to fetch the definition to draw itself."""
    as_user(client, agency)
    pid = client.post("/api/projects", json={"display_name": f"Ref {next(_n)}"}).json()["id"]
    key = client.post("/api/admin/field-definitions",
                      json={"label": f"Hosting plan {next(_n)}", "type": "select",
                            "options": ["Managed", "Self-hosted"],
                            "confirm": True}).json()["key"]
    client.put(f"/api/projects/{pid}/fields/{key}", json={"value": "Managed"})
    f = client.get(f"/api/projects/{pid}").json()["fields"][0]
    assert f["type"] == "select" and f["options"] == ["Managed", "Self-hosted"]
