from __future__ import annotations

import hashlib
import importlib.util
import json
from pathlib import Path
from types import SimpleNamespace

import pytest
import yaml


REPO_ROOT = Path(__file__).resolve().parents[2]
MODULE_PATH = REPO_ROOT / "scripts" / "rho-controlled-action-relay.py"


def load_module():
    spec = importlib.util.spec_from_file_location("rho_controlled_action_relay", MODULE_PATH)
    assert spec is not None
    assert spec.loader is not None
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


relay = load_module()


def valid_run_action() -> dict:
    return {
        "version": 1,
        "kind": "controlled_action",
        "action": {
            "action_id": "act-run-real-123",
            "request_id": "req-12345678-1234-1234-1234-1234567890ab",
            "requested_by": "test-agent2",
            "requested_for": "test-agent1",
            "action_type": "run_real_data",
            "summary": "Run the reviewed script.",
            "reason": "Mock run succeeded.",
            "input_path": "/input/private/prices-real.csv",
            "output_path": "/results/private/output.txt",
            "script_path": "/output/sum_prices.py",
            "message_path": "/messages/agent2-to-agent1.txt",
            "supporting_paths": [
                "/messages/agent2-to-agent1.txt",
                "/output/sum_prices.py",
            ],
            "template_vars": [{"name": "dataset_id", "value": "abc"}],
        },
    }


def valid_release_action() -> dict:
    payload = valid_run_action()
    payload["action"] = {
        "action_id": "act-release-results-123",
        "request_id": "req-12345678-1234-1234-1234-1234567890ab",
        "requested_by": "test-agent1",
        "requested_for": "test-agent1",
        "action_type": "release_results",
        "summary": "Release the reviewed result.",
        "reason": "Run completed.",
        "input_path": "/results/private/output.txt",
        "output_path": "/release/shared/output.txt",
        "manifest_path": "/release/meta/req-12345678-1234-1234-1234-1234567890ab.json",
        "notification_path": "/release/notify/result-12345678.json",
        "supporting_paths": ["/results/private/output.txt"],
        "template_vars": [],
    }
    return payload


def test_validate_action_payload_accepts_run_action():
    result = relay.validate_action_payload(valid_run_action())
    assert result["action_type"] == "run_real_data"
    assert result["script_path"] == "/output/sum_prices.py"


def test_validate_action_payload_accepts_release_action():
    result = relay.validate_action_payload(valid_release_action())
    assert result["action_type"] == "release_results"
    assert result["manifest_path"].endswith(".json")


def test_validate_action_payload_rejects_parent_traversal():
    payload = valid_run_action()
    payload["action"]["output_path"] = "/results/private/../secret.txt"
    with pytest.raises(SystemExit):
        relay.validate_action_payload(payload)


def test_validate_action_payload_requires_release_fields():
    payload = valid_release_action()
    del payload["action"]["manifest_path"]
    with pytest.raises(SystemExit):
        relay.validate_action_payload(payload)


def test_parse_mappings_and_map_guest_to_host():
    mappings = relay.parse_mappings(
        [
            "/results/private=/tmp/private-results",
            "/release/shared=/tmp/shared-results",
        ]
    )
    mapped = relay.map_guest_to_host("/results/private/output.txt", mappings)
    assert str(mapped).endswith("/tmp/private-results/output.txt")


def test_decide_via_fixture_uses_action_type_default(tmp_path: Path):
    fixture = tmp_path / "policy.yaml"
    fixture.write_text(
        "version: 1\ndefault_decision: approved\nactions:\n  release_results: denied\n",
        encoding="utf-8",
    )
    args = SimpleNamespace(fixture_file=fixture)
    decision, actor = relay.decide_via_fixture(args, relay.validate_action_payload(valid_release_action()))
    assert decision == "denied"
    assert actor == "fixture-middleware"


def test_decide_via_fixture_prefers_action_id(tmp_path: Path):
    fixture = tmp_path / "policy.yaml"
    fixture.write_text(
        "version: 1\ndefault_decision: denied\nactions:\n  act-run-real-123: approved\n",
        encoding="utf-8",
    )
    args = SimpleNamespace(fixture_file=fixture)
    decision, actor = relay.decide_via_fixture(args, relay.validate_action_payload(valid_run_action()))
    assert decision == "approved"
    assert actor == "fixture-middleware"


# --- Action manifest binding tests ---


def _write_manifest(manifest_dir: Path, request_id: str, expected: dict) -> Path:
    path = manifest_dir / f"{request_id}.yaml"
    manifest_dir.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8") as handle:
        yaml.safe_dump({"expected_action": expected}, handle, sort_keys=False)
    return path


REQUEST_ID = "req-12345678-1234-1234-1234-1234567890ab"


def test_load_action_manifest_returns_none_when_missing(tmp_path: Path):
    result = relay.load_action_manifest(tmp_path, REQUEST_ID)
    assert result is None


def test_load_action_manifest_returns_expected_action(tmp_path: Path):
    _write_manifest(tmp_path, REQUEST_ID, {"action_type": "run_real_data"})
    result = relay.load_action_manifest(tmp_path, REQUEST_ID)
    assert result is not None
    assert result["action_type"] == "run_real_data"


def test_validate_against_manifest_accepts_matching_action():
    action = relay.validate_action_payload(valid_run_action())
    manifest = {
        "request_id": REQUEST_ID,
        "action_type": "run_real_data",
        "allowed_input_paths": ["/input/private/prices-real.csv"],
        "allowed_script_paths": ["/output/sum_prices.py"],
        "allowed_output_paths": ["/results/private/output.txt"],
        "required_requested_by": "test-agent2",
        "required_requested_for": "test-agent1",
    }
    relay.validate_against_manifest(action, manifest)


def test_validate_against_manifest_rejects_wrong_request_id():
    action = relay.validate_action_payload(valid_run_action())
    manifest = {"request_id": "req-00000000-0000-0000-0000-000000000000"}
    with pytest.raises(SystemExit, match="request_id mismatch"):
        relay.validate_against_manifest(action, manifest)


def test_validate_against_manifest_rejects_wrong_action_type():
    action = relay.validate_action_payload(valid_run_action())
    manifest = {"action_type": "release_results"}
    with pytest.raises(SystemExit, match="action_type mismatch"):
        relay.validate_against_manifest(action, manifest)


def test_validate_against_manifest_rejects_wrong_input_path():
    action = relay.validate_action_payload(valid_run_action())
    manifest = {"allowed_input_paths": ["/input/private/other-file.csv"]}
    with pytest.raises(SystemExit, match="input_path.*not in allowed set"):
        relay.validate_against_manifest(action, manifest)


def test_validate_against_manifest_rejects_wrong_script_path():
    action = relay.validate_action_payload(valid_run_action())
    manifest = {"allowed_script_paths": ["/output/other_script.py"]}
    with pytest.raises(SystemExit, match="script_path.*not in allowed set"):
        relay.validate_against_manifest(action, manifest)


def test_validate_against_manifest_rejects_wrong_output_path():
    action = relay.validate_action_payload(valid_run_action())
    manifest = {"allowed_output_paths": ["/results/private/different.txt"]}
    with pytest.raises(SystemExit, match="output_path.*not in allowed set"):
        relay.validate_against_manifest(action, manifest)


def test_validate_against_manifest_rejects_wrong_requested_by():
    action = relay.validate_action_payload(valid_run_action())
    manifest = {"required_requested_by": "other-agent"}
    with pytest.raises(SystemExit, match="requested_by mismatch"):
        relay.validate_against_manifest(action, manifest)


def test_validate_against_manifest_rejects_wrong_requested_for():
    action = relay.validate_action_payload(valid_run_action())
    manifest = {"required_requested_for": "other-agent"}
    with pytest.raises(SystemExit, match="requested_for mismatch"):
        relay.validate_against_manifest(action, manifest)


def test_validate_against_manifest_allows_multiple_input_paths():
    action = relay.validate_action_payload(valid_run_action())
    manifest = {
        "allowed_input_paths": [
            "/input/private/other.csv",
            "/input/private/prices-real.csv",
        ]
    }
    relay.validate_against_manifest(action, manifest)


def test_validate_against_manifest_skips_absent_fields():
    action = relay.validate_action_payload(valid_run_action())
    manifest = {"request_id": REQUEST_ID}
    relay.validate_against_manifest(action, manifest)


# --- Code digest verification tests ---


def test_sha256_file(tmp_path: Path):
    target = tmp_path / "hello.txt"
    target.write_text("hello world\n", encoding="utf-8")
    expected = hashlib.sha256(b"hello world\n").hexdigest()
    assert relay.sha256_file(target) == expected


def test_verify_code_digest_passes_on_match(tmp_path: Path):
    script = tmp_path / "output" / "sum_prices.py"
    script.parent.mkdir(parents=True)
    script.write_text("print('ok')\n", encoding="utf-8")
    digest = hashlib.sha256(b"print('ok')\n").hexdigest()
    action = relay.validate_action_payload(valid_run_action())
    manifest = {"code_digest": digest}
    mappings = relay.parse_mappings([f"/output={tmp_path / 'output'}"])
    relay.verify_code_digest(action, manifest, mappings)


def test_verify_code_digest_fails_on_mismatch(tmp_path: Path):
    script = tmp_path / "output" / "sum_prices.py"
    script.parent.mkdir(parents=True)
    script.write_text("print('tampered')\n", encoding="utf-8")
    action = relay.validate_action_payload(valid_run_action())
    manifest = {"code_digest": "0000000000000000000000000000000000000000000000000000000000000000"}
    mappings = relay.parse_mappings([f"/output={tmp_path / 'output'}"])
    with pytest.raises(SystemExit, match="code digest mismatch"):
        relay.verify_code_digest(action, manifest, mappings)


def test_verify_code_digest_fails_on_missing_file(tmp_path: Path):
    action = relay.validate_action_payload(valid_run_action())
    manifest = {"code_digest": "abcd1234"}
    mappings = relay.parse_mappings([f"/output={tmp_path / 'output'}"])
    with pytest.raises(SystemExit, match="script file not found"):
        relay.verify_code_digest(action, manifest, mappings)


def test_verify_code_digest_skipped_when_no_digest():
    action = relay.validate_action_payload(valid_run_action())
    manifest = {}
    relay.verify_code_digest(action, manifest, {})


def test_verify_code_digest_skipped_when_no_script_path():
    action = relay.validate_action_payload(valid_release_action())
    manifest = {"code_digest": "abcd1234"}
    relay.verify_code_digest(action, manifest, {})


# --- Full relay integration with manifest binding ---


def test_cmd_relay_rejects_when_manifest_binding_fails(tmp_path: Path):
    control_root = tmp_path / "control"
    outbox = control_root / "outbox"
    outbox.mkdir(parents=True)
    shared_root = tmp_path / "shared"
    shared_root.mkdir()
    manifest_dir = tmp_path / "manifests"

    payload = valid_run_action()
    action_id = payload["action"]["action_id"]
    (outbox / f"{action_id}.json").write_text(
        json.dumps(payload, indent=2) + "\n", encoding="utf-8"
    )

    _write_manifest(
        manifest_dir,
        REQUEST_ID,
        {
            "request_id": REQUEST_ID,
            "action_type": "run_real_data",
            "allowed_input_paths": ["/input/private/WRONG-file.csv"],
        },
    )

    args = SimpleNamespace(
        control_root=control_root,
        shared_root=shared_root,
        action_id=action_id,
        middleware="auto",
        manifest_dir=manifest_dir,
        require_manifest=False,
        mode=None,
        telegram_profile=None,
        telegram_root=None,
        telegram_user=None,
        fixture_file=None,
        timeout_seconds=10,
        map=[],
    )

    with pytest.raises(SystemExit, match="input_path.*not in allowed set"):
        relay.cmd_relay(args)


def test_cmd_relay_rejects_when_manifest_required_but_missing(tmp_path: Path):
    control_root = tmp_path / "control"
    outbox = control_root / "outbox"
    outbox.mkdir(parents=True)
    shared_root = tmp_path / "shared"
    shared_root.mkdir()
    manifest_dir = tmp_path / "manifests"
    manifest_dir.mkdir()

    payload = valid_run_action()
    action_id = payload["action"]["action_id"]
    (outbox / f"{action_id}.json").write_text(
        json.dumps(payload, indent=2) + "\n", encoding="utf-8"
    )

    args = SimpleNamespace(
        control_root=control_root,
        shared_root=shared_root,
        action_id=action_id,
        middleware="auto",
        manifest_dir=manifest_dir,
        require_manifest=True,
        mode=None,
        telegram_profile=None,
        telegram_root=None,
        telegram_user=None,
        fixture_file=None,
        timeout_seconds=10,
        map=[],
    )

    with pytest.raises(SystemExit, match="no action manifest found"):
        relay.cmd_relay(args)


# --- Approval message display tests ---


def test_build_action_message_without_manifest():
    action = relay.validate_action_payload(valid_run_action())
    message = relay.build_action_message(action)
    assert "Host-verified manifest" not in message
    assert "Agent-generated (unverified)" not in message
    assert "Summary: Run the reviewed script." in message
    assert "/approve" in message
    assert "/deny" in message


def test_build_action_message_with_manifest():
    action = relay.validate_action_payload(valid_run_action())
    manifest = {
        "allowed_input_paths": ["/input/private/prices-real.csv"],
        "allowed_script_paths": ["/output/sum_prices.py"],
        "allowed_output_paths": ["/results/private/output.txt"],
        "code_digest": "abc123def456",
    }
    message = relay.build_action_message(action, manifest)
    assert "Host-verified manifest" in message
    assert "Agent-proposed values" in message
    assert "Agent-generated (unverified)" in message
    assert "/input/private/prices-real.csv" in message
    assert "abc123def456" in message
    assert "Summary: Run the reviewed script." in message
    assert "/approve" in message


def test_build_action_message_manifest_shows_code_digest_none():
    action = relay.validate_action_payload(valid_run_action())
    manifest = {"allowed_input_paths": []}
    message = relay.build_action_message(action, manifest)
    assert "Code digest: none" in message
