"""Tests for C2 config CRUD endpoints.

Covers:
- GET 404 when no config exists
- PUT create (api_token required)
- PUT update with omitted token keeps old ciphertext
- GET 200 returns has_token=True, never cleartext
- DELETE 204
- Cascade delete when engagement is deleted
- RBAC: admin OK / redteam OK / SOC 403 on all 4 endpoints
- 503 guard when MIMIC_ENCRYPTION_KEY is unset
- POST /test with fake adapter
"""
from __future__ import annotations

import pytest
from cryptography.fernet import Fernet
from flask import Flask
from flask.testing import FlaskClient

from backend.app.models.c2_config import C2Config
from backend.tests.conftest import auth_headers as _h

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

_FERNET_KEY = Fernet.generate_key().decode()


@pytest.fixture(autouse=True)
def set_encryption_key(monkeypatch):
    """Default: key is present. Individual tests can override."""
    monkeypatch.setenv("MIMIC_ENCRYPTION_KEY", _FERNET_KEY)


@pytest.fixture(autouse=True)
def use_fake_adapter(monkeypatch):
    monkeypatch.setenv("MIMIC_C2_ADAPTER", "fake")


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


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


def _put_config(
    client: FlaskClient,
    token: str,
    eid: int,
    *,
    url: str = "https://c2.internal:7443",
    api_token: str | None = "s3cr3t",
    verify_tls: bool = True,
) -> dict:
    payload: dict = {"url": url, "verify_tls": verify_tls}
    if api_token is not None:
        payload["api_token"] = api_token
    resp = client.put(
        f"/api/engagements/{eid}/c2-config",
        headers=_h(token),
        json=payload,
    )
    return resp


# ---------------------------------------------------------------------------
# GET — 404 when no config
# ---------------------------------------------------------------------------


def test_get_config_not_found(client: FlaskClient, admin_token: str) -> None:
    eng = _make_engagement(client, admin_token)
    resp = client.get(f"/api/engagements/{eng['id']}/c2-config", headers=_h(admin_token))
    assert resp.status_code == 404


def test_get_config_engagement_not_found(client: FlaskClient, admin_token: str) -> None:
    resp = client.get("/api/engagements/9999/c2-config", headers=_h(admin_token))
    assert resp.status_code == 404


# ---------------------------------------------------------------------------
# PUT — create
# ---------------------------------------------------------------------------


def test_put_rejects_http_scheme(client: FlaskClient, admin_token: str) -> None:
    eng = _make_engagement(client, admin_token)
    resp = _put_config(client, admin_token, eng["id"], url="http://c2.internal:7443")
    assert resp.status_code == 400
    assert "https" in resp.get_json().get("error", "").lower()


def test_put_rejects_missing_hostname(client: FlaskClient, admin_token: str) -> None:
    eng = _make_engagement(client, admin_token)
    # urlparse("https://:7443") produces an empty hostname
    resp = _put_config(client, admin_token, eng["id"], url="https://:7443")
    assert resp.status_code == 400
    assert "hostname" in resp.get_json().get("error", "").lower()


def test_put_creates_config(client: FlaskClient, admin_token: str) -> None:
    eng = _make_engagement(client, admin_token)
    resp = _put_config(client, admin_token, eng["id"])
    assert resp.status_code == 200
    body = resp.get_json()
    assert body["has_token"] is True
    assert body["url"] == "https://c2.internal:7443"
    assert body["verify_tls"] is True


def test_put_create_requires_api_token(client: FlaskClient, admin_token: str) -> None:
    eng = _make_engagement(client, admin_token)
    resp = _put_config(client, admin_token, eng["id"], api_token=None)
    assert resp.status_code == 400


def test_put_create_requires_url(client: FlaskClient, admin_token: str) -> None:
    eng = _make_engagement(client, admin_token)
    resp = client.put(
        f"/api/engagements/{eng['id']}/c2-config",
        headers=_h(admin_token),
        json={"api_token": "tok", "verify_tls": True},
    )
    assert resp.status_code == 400


# ---------------------------------------------------------------------------
# PUT — update, omitting api_token preserves old ciphertext
# ---------------------------------------------------------------------------


def test_put_update_omits_token_keeps_old(
    app: Flask, client: FlaskClient, admin_token: str
) -> None:
    eng = _make_engagement(client, admin_token)
    _put_config(client, admin_token, eng["id"], api_token="original-token")

    # Read ciphertext from DB before update.
    with app.app_context():
        cfg = C2Config.query.filter_by(engagement_id=eng["id"]).first()
        assert cfg is not None
        old_cipher = cfg.api_token_encrypted

    # Update URL, omit api_token.
    resp = _put_config(
        client, admin_token, eng["id"],
        url="https://new.internal:7443", api_token=None,
    )
    assert resp.status_code == 200

    with app.app_context():
        cfg = C2Config.query.filter_by(engagement_id=eng["id"]).first()
        assert cfg is not None
        assert cfg.api_token_encrypted == old_cipher
        assert cfg.url == "https://new.internal:7443"


def test_put_update_with_token_replaces_ciphertext(
    app: Flask, client: FlaskClient, admin_token: str
) -> None:
    eng = _make_engagement(client, admin_token)
    _put_config(client, admin_token, eng["id"], api_token="original-token")

    with app.app_context():
        cfg = C2Config.query.filter_by(engagement_id=eng["id"]).first()
        assert cfg is not None
        old_cipher = cfg.api_token_encrypted

    _put_config(client, admin_token, eng["id"], api_token="new-token")

    with app.app_context():
        cfg = C2Config.query.filter_by(engagement_id=eng["id"]).first()
        assert cfg is not None
        assert cfg.api_token_encrypted != old_cipher


# ---------------------------------------------------------------------------
# GET — 200, has_token=True, never cleartext
# ---------------------------------------------------------------------------


def test_get_config_returns_has_token_not_cleartext(
    client: FlaskClient, admin_token: str
) -> None:
    eng = _make_engagement(client, admin_token)
    _put_config(client, admin_token, eng["id"], api_token="s3cr3t")

    resp = client.get(f"/api/engagements/{eng['id']}/c2-config", headers=_h(admin_token))
    assert resp.status_code == 200
    body = resp.get_json()
    assert body["has_token"] is True
    assert "api_token" not in body
    assert "api_token_encrypted" not in body
    assert "s3cr3t" not in str(body)


def test_get_config_verify_tls_default_false(
    client: FlaskClient, admin_token: str
) -> None:
    eng = _make_engagement(client, admin_token)
    _put_config(client, admin_token, eng["id"], verify_tls=False)
    resp = client.get(f"/api/engagements/{eng['id']}/c2-config", headers=_h(admin_token))
    assert resp.get_json()["verify_tls"] is False


# ---------------------------------------------------------------------------
# DELETE — 204
# ---------------------------------------------------------------------------


def test_delete_config_204(client: FlaskClient, admin_token: str) -> None:
    eng = _make_engagement(client, admin_token)
    _put_config(client, admin_token, eng["id"])

    resp = client.delete(
        f"/api/engagements/{eng['id']}/c2-config", headers=_h(admin_token)
    )
    assert resp.status_code == 204

    # Subsequent GET returns 404.
    resp2 = client.get(f"/api/engagements/{eng['id']}/c2-config", headers=_h(admin_token))
    assert resp2.status_code == 404


def test_delete_config_not_found(client: FlaskClient, admin_token: str) -> None:
    eng = _make_engagement(client, admin_token)
    resp = client.delete(
        f"/api/engagements/{eng['id']}/c2-config", headers=_h(admin_token)
    )
    assert resp.status_code == 404


# ---------------------------------------------------------------------------
# CASCADE — delete engagement removes config
# ---------------------------------------------------------------------------


def test_cascade_delete_engagement_removes_config(
    app: Flask, client: FlaskClient, admin_token: str
) -> None:
    eng = _make_engagement(client, admin_token)
    _put_config(client, admin_token, eng["id"])

    with app.app_context():
        assert C2Config.query.filter_by(engagement_id=eng["id"]).count() == 1

    client.delete(f"/api/engagements/{eng['id']}", headers=_h(admin_token))

    with app.app_context():
        assert C2Config.query.filter_by(engagement_id=eng["id"]).count() == 0


# ---------------------------------------------------------------------------
# RBAC matrix
# ---------------------------------------------------------------------------


@pytest.mark.parametrize("method,path_suffix", [
    ("GET",    "/c2-config"),
    ("PUT",    "/c2-config"),
    ("DELETE", "/c2-config"),
    ("POST",   "/c2-config/test"),
])
def test_soc_gets_403(
    client: FlaskClient, admin_token: str, soc_token: str,
    method: str, path_suffix: str,
) -> None:
    eng = _make_engagement(client, admin_token)
    url = f"/api/engagements/{eng['id']}{path_suffix}"
    resp = getattr(client, method.lower())(url, headers=_h(soc_token), json={})
    assert resp.status_code == 403


@pytest.mark.parametrize("method,path_suffix", [
    ("GET",    "/c2-config"),
    ("DELETE", "/c2-config"),
    ("POST",   "/c2-config/test"),
])
def test_redteam_gets_allowed(
    client: FlaskClient, admin_token: str, redteam_token: str,
    method: str, path_suffix: str,
) -> None:
    eng = _make_engagement(client, admin_token)
    # Ensure config exists for GET/DELETE/test.
    _put_config(client, admin_token, eng["id"])
    url = f"/api/engagements/{eng['id']}{path_suffix}"
    resp = getattr(client, method.lower())(url, headers=_h(redteam_token), json={})
    # Not 403 and not 401.
    assert resp.status_code not in (401, 403)


def test_redteam_can_put_config(
    client: FlaskClient, admin_token: str, redteam_token: str,
) -> None:
    eng = _make_engagement(client, admin_token)
    resp = _put_config(client, redteam_token, eng["id"])
    assert resp.status_code == 200


# ---------------------------------------------------------------------------
# 503 guard when MIMIC_ENCRYPTION_KEY is unset
# ---------------------------------------------------------------------------


@pytest.mark.parametrize("method,path_suffix", [
    ("GET",    "/c2-config"),
    ("PUT",    "/c2-config"),
    ("DELETE", "/c2-config"),
    ("POST",   "/c2-config/test"),
])
def test_503_when_key_unset(
    monkeypatch,
    client: FlaskClient,
    admin_token: str,
    method: str,
    path_suffix: str,
) -> None:
    monkeypatch.delenv("MIMIC_ENCRYPTION_KEY", raising=False)
    eng = _make_engagement(client, admin_token)
    url = f"/api/engagements/{eng['id']}{path_suffix}"
    resp = getattr(client, method.lower())(url, headers=_h(admin_token), json={
        "url": "https://c2", "api_token": "tok", "verify_tls": True,
    })
    assert resp.status_code == 503
    assert "MIMIC_ENCRYPTION_KEY" in resp.get_json().get("error", "")


# ---------------------------------------------------------------------------
# POST /test — connectivity check via fake adapter
# ---------------------------------------------------------------------------


def test_post_test_returns_ok_true(client: FlaskClient, admin_token: str) -> None:
    eng = _make_engagement(client, admin_token)
    _put_config(client, admin_token, eng["id"])

    resp = client.post(
        f"/api/engagements/{eng['id']}/c2-config/test",
        headers=_h(admin_token),
        json={},
    )
    assert resp.status_code == 200
    body = resp.get_json()
    assert body["ok"] is True
    assert body["error"] is None


def test_post_test_no_config_returns_404(client: FlaskClient, admin_token: str) -> None:
    eng = _make_engagement(client, admin_token)
    resp = client.post(
        f"/api/engagements/{eng['id']}/c2-config/test",
        headers=_h(admin_token),
        json={},
    )
    assert resp.status_code == 404
