"""Sprint 4 — tactic_ids PATCH tests (AC-21)."""
from __future__ import annotations

import pathlib

import pytest
from flask.testing import FlaskClient

from backend.app.services import mitre as mitre_svc
from backend.tests.conftest import auth_headers as _h

_FIXTURE_BUNDLE = {
    "type": "bundle",
    "objects": [
        {
            "type": "attack-pattern",
            "name": "Command and Scripting Interpreter",
            "external_references": [{"source_name": "mitre-attack", "external_id": "T1059"}],
            "kill_chain_phases": [{"phase_name": "execution", "kill_chain_name": "mitre-attack"}],
        },
    ],
}


@pytest.fixture(autouse=True)
def _reset_mitre():
    original_loaded = mitre_svc.mitre_loaded
    original_index = list(mitre_svc._index)
    original_tactics = dict(mitre_svc._tactics_by_technique)
    original_names = dict(mitre_svc._name_by_id)
    original_matrix = list(mitre_svc._matrix)
    yield
    mitre_svc.mitre_loaded = original_loaded
    mitre_svc._index = original_index
    mitre_svc._tactics_by_technique = original_tactics
    mitre_svc._name_by_id = original_names
    mitre_svc._matrix = original_matrix


@pytest.fixture()
def bundle_file(tmp_path: pathlib.Path) -> pathlib.Path:
    import json
    p = tmp_path / "enterprise-attack.json"
    p.write_text(json.dumps(_FIXTURE_BUNDLE), encoding="utf-8")
    return p


def _make_engagement(client: FlaskClient, token: str) -> dict:
    resp = client.post(
        "/api/engagements",
        headers=_h(token),
        json={"name": "Eng", "start_date": "2026-01-01"},
    )
    assert resp.status_code == 201
    return resp.get_json()


def _make_sim(client: FlaskClient, token: str, eid: int) -> dict:
    resp = client.post(
        f"/api/engagements/{eid}/simulations",
        headers=_h(token),
        json={"name": "Sim"},
    )
    assert resp.status_code == 201
    return resp.get_json()


def _patch(client: FlaskClient, token: str, sid: int, payload: dict):
    return client.patch(
        f"/api/simulations/{sid}",
        headers=_h(token),
        json=payload,
    )


# ---------------------------------------------------------------------------
# tactic_ids happy path
# ---------------------------------------------------------------------------


def test_patch_tactic_ids_valid(
    client: FlaskClient, redteam_token: str
) -> None:
    eng = _make_engagement(client, redteam_token)
    sim = _make_sim(client, redteam_token, eng["id"])

    resp = _patch(client, redteam_token, sim["id"], {"tactic_ids": ["TA0007"]})
    assert resp.status_code == 200
    data = resp.get_json()
    assert data["tactics"] == [{"id": "TA0007", "name": "Discovery"}]


def test_patch_tactic_ids_multiple(
    client: FlaskClient, redteam_token: str
) -> None:
    eng = _make_engagement(client, redteam_token)
    sim = _make_sim(client, redteam_token, eng["id"])

    resp = _patch(client, redteam_token, sim["id"], {"tactic_ids": ["TA0001", "TA0002"]})
    assert resp.status_code == 200
    tactics = resp.get_json()["tactics"]
    ids = [t["id"] for t in tactics]
    assert "TA0001" in ids
    assert "TA0002" in ids


def test_patch_tactic_ids_empty_clears(
    client: FlaskClient, redteam_token: str
) -> None:
    eng = _make_engagement(client, redteam_token)
    sim = _make_sim(client, redteam_token, eng["id"])
    _patch(client, redteam_token, sim["id"], {"tactic_ids": ["TA0007"]})

    resp = _patch(client, redteam_token, sim["id"], {"tactic_ids": []})
    assert resp.status_code == 200
    assert resp.get_json()["tactics"] == []


def test_patch_tactic_ids_dedup(
    client: FlaskClient, redteam_token: str
) -> None:
    eng = _make_engagement(client, redteam_token)
    sim = _make_sim(client, redteam_token, eng["id"])

    resp = _patch(client, redteam_token, sim["id"], {"tactic_ids": ["TA0007", "TA0007"]})
    assert resp.status_code == 200
    tactics = resp.get_json()["tactics"]
    assert len(tactics) == 1


# ---------------------------------------------------------------------------
# tactic_ids error paths
# ---------------------------------------------------------------------------


def test_patch_tactic_ids_unknown_returns_400(
    client: FlaskClient, redteam_token: str
) -> None:
    eng = _make_engagement(client, redteam_token)
    sim = _make_sim(client, redteam_token, eng["id"])

    resp = _patch(client, redteam_token, sim["id"], {"tactic_ids": ["TA9999"]})
    assert resp.status_code == 400
    assert "unknown tactic id" in resp.get_json()["error"]


def test_patch_tactic_ids_not_a_list_returns_400(
    client: FlaskClient, redteam_token: str
) -> None:
    eng = _make_engagement(client, redteam_token)
    sim = _make_sim(client, redteam_token, eng["id"])

    resp = _patch(client, redteam_token, sim["id"], {"tactic_ids": "TA0007"})
    assert resp.status_code == 400


# ---------------------------------------------------------------------------
# SOC gate
# ---------------------------------------------------------------------------


def test_soc_cannot_patch_tactic_ids(
    client: FlaskClient, redteam_token: str, soc_token: str
) -> None:
    eng = _make_engagement(client, redteam_token)
    sim = _make_sim(client, redteam_token, eng["id"])
    # Advance to review_required so SOC can act.
    client.post(
        f"/api/simulations/{sim['id']}/transition",
        headers=_h(redteam_token),
        json={"to": "review_required"},
    )

    resp = _patch(client, soc_token, sim["id"], {"tactic_ids": ["TA0007"]})
    assert resp.status_code == 403


# ---------------------------------------------------------------------------
# Auto-transition via tactic_ids
# ---------------------------------------------------------------------------


def test_patch_tactic_ids_triggers_auto_transition(
    client: FlaskClient, redteam_token: str
) -> None:
    eng = _make_engagement(client, redteam_token)
    sim = _make_sim(client, redteam_token, eng["id"])
    assert sim["status"] == "pending"

    resp = _patch(client, redteam_token, sim["id"], {"tactic_ids": ["TA0007"]})
    assert resp.status_code == 200
    assert resp.get_json()["status"] == "in_progress"


def test_patch_empty_tactic_ids_no_auto_transition(
    client: FlaskClient, redteam_token: str
) -> None:
    eng = _make_engagement(client, redteam_token)
    sim = _make_sim(client, redteam_token, eng["id"])

    resp = _patch(client, redteam_token, sim["id"], {"tactic_ids": []})
    assert resp.status_code == 200
    assert resp.get_json()["status"] == "pending"


# ---------------------------------------------------------------------------
# tactic_ids not affected by MITRE bundle loaded state
# (validation uses hardcoded _TACTIC_IDS, not the live bundle)
# ---------------------------------------------------------------------------


def test_patch_tactic_ids_works_without_bundle(
    client: FlaskClient, redteam_token: str
) -> None:
    """tactic_ids validation is hardcoded — bundle state is irrelevant."""
    mitre_svc.mitre_loaded = False
    mitre_svc._index = []

    eng = _make_engagement(client, redteam_token)
    sim = _make_sim(client, redteam_token, eng["id"])

    resp = _patch(client, redteam_token, sim["id"], {"tactic_ids": ["TA0007"]})
    assert resp.status_code == 200


def test_patch_technique_ids_bundle_not_loaded_returns_503(
    client: FlaskClient, redteam_token: str
) -> None:
    """technique_ids still needs the bundle (different from tactic_ids)."""
    mitre_svc.mitre_loaded = False
    mitre_svc._index = []

    eng = _make_engagement(client, redteam_token)
    sim = _make_sim(client, redteam_token, eng["id"])

    resp = _patch(client, redteam_token, sim["id"], {"technique_ids": ["T1059"]})
    assert resp.status_code == 503
