import json
import os
import subprocess
import sys
import time
from collections.abc import Mapping
from pathlib import Path

import pytest

from ops import verify_gcp_leoclean_service_environment as verifier


def environment_block(environment: Mapping[str, str]) -> bytes:
    return b"\0".join(f"{key}={value}".encode() for key, value in environment.items()) + b"\0"


def wait_for_process_environment(
    process: subprocess.Popen[bytes],
    expected: Mapping[str, str],
    *,
    timeout_seconds: float = 5.0,
) -> None:
    deadline = time.monotonic() + timeout_seconds
    while time.monotonic() < deadline:
        if process.poll() is not None:
            pytest.fail("environment fixture process exited before becoming readable")
        try:
            observed = verifier.parse_environment(Path(f"/proc/{process.pid}/environ").read_bytes())
        except (OSError, verifier.VerificationError):
            time.sleep(0.01)
            continue
        if observed == expected:
            return
        time.sleep(0.01)
    pytest.fail("environment fixture process did not become readable")


def test_all_expected_fields_pass_and_receipt_contains_no_values() -> None:
    environment = verifier.expected_environment()
    environment.update({"HOME": "/home/teleo", "LANG": "C.UTF-8"})

    parsed = verifier.parse_environment(environment_block(environment))
    validated = verifier.validate_environment(parsed)
    receipt = {
        "runtime_environment": "scoped",
        "status": "pass",
        "validated_fields": list(validated),
    }
    rendered = verifier.canonical_json(receipt)

    assert validated == tuple(sorted(verifier.expected_environment()))
    assert json.loads(rendered) == receipt
    assert rendered == json.dumps(receipt, ensure_ascii=True, separators=(",", ":"), sort_keys=True) + "\n"
    for value in verifier.expected_environment().values():
        assert value not in rendered


@pytest.mark.parametrize("field", sorted(verifier.expected_environment()))
def test_each_expected_field_mismatch_fails_closed(field: str) -> None:
    environment = verifier.expected_environment()
    environment[field] = "wrong-value-that-is-not-a-secret"

    with pytest.raises(verifier.VerificationError) as caught:
        verifier.validate_environment(environment)

    assert caught.value.code == "environment_mismatch"
    assert caught.value.fields == (field,)


@pytest.mark.parametrize(
    ("field", "unsafe_value", "expected_code"),
    [
        ("TELEO_CLOUDSQL_USER", "postgres", "environment_mismatch"),
        (
            "TELEO_CLOUDSQL_PASSWORD_SECRET",
            verifier.ADMINISTRATOR_PASSWORD_SECRET,
            "administrator_secret_reference",
        ),
    ],
)
def test_later_dropin_effective_override_fails_without_outputting_value(
    field: str,
    unsafe_value: str,
    expected_code: str,
) -> None:
    environment = verifier.expected_environment()
    environment[field] = unsafe_value

    with pytest.raises(verifier.VerificationError) as caught:
        verifier.validate_environment(environment)

    rendered = verifier.canonical_json(verifier.failure_receipt(caught.value))
    assert caught.value.code == expected_code
    assert unsafe_value not in rendered


@pytest.mark.parametrize(
    ("field", "safe_label"),
    [
        ("PGPASSWORD", "PG*"),
        ("pgservice", "PG*"),
        ("CLOUDSDK_CORE_PROJECT", "CLOUDSDK_*"),
        ("CLOUDSDK_AUTH_ACCESS_TOKEN", "CLOUDSDK_*"),
        ("cloudsdk_auth_refresh_token", "CLOUDSDK_*"),
        ("GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_APPLICATION_CREDENTIALS"),
        ("LD_PRELOAD", "LD_*"),
        ("LD_LIBRARY_PATH", "LD_*"),
        ("ld_audit", "LD_*"),
        ("BASH_ENV", "BASH_ENV"),
        ("ENV", "ENV"),
        ("BASH_FUNC_injected%%", "BASH_FUNC_*"),
        ("PYTHONHOME", "PYTHON*"),
        ("PYTHONPATH", "PYTHON*"),
        ("PYTHONHTTPSVERIFY", "PYTHON*"),
        ("pythonstartup", "PYTHON*"),
        ("HTTP_PROXY", "HTTP_PROXY"),
        ("https_proxy", "HTTPS_PROXY"),
        ("ALL_PROXY", "ALL_PROXY"),
        ("no_proxy", "NO_PROXY"),
        ("SSL_CERT_FILE", "SSL_CERT_FILE"),
        ("ssl_cert_dir", "SSL_CERT_DIR"),
        ("REQUESTS_CA_BUNDLE", "REQUESTS_CA_BUNDLE"),
        ("curl_ca_bundle", "CURL_CA_BUNDLE"),
        ("SSLKEYLOGFILE", "SSLKEYLOGFILE"),
        ("TELEO_CLOUDSQL_CREDENTIAL_MODE", "TELEO_CLOUDSQL_CREDENTIAL_MODE"),
        ("TELEO_KB_CLAIM_BASE_URL", "TELEO_KB_CLAIM_BASE_URL"),
        ("TELEO_KB_READ_ATTEMPTS", "TELEO_KB_READ_ATTEMPTS"),
        ("TELEO_MEMORY_INCLUDE_EXCERPTS", "TELEO_MEMORY_INCLUDE_EXCERPTS"),
        ("TELEO_MEMORY_REDACT", "TELEO_MEMORY_REDACT"),
    ],
)
def test_environment_file_broader_credentials_and_forbidden_fields_fail_closed(
    field: str,
    safe_label: str,
) -> None:
    environment = verifier.expected_environment()
    injected_value = "sensitive-injected-value"
    environment[field] = injected_value

    with pytest.raises(verifier.VerificationError) as caught:
        verifier.validate_environment(environment)

    rendered = verifier.canonical_json(verifier.failure_receipt(caught.value))
    assert caught.value.code == "forbidden_environment_fields"
    assert caught.value.fields == (safe_label,)
    assert injected_value not in rendered


def test_arbitrary_forbidden_key_suffix_is_not_an_output_channel() -> None:
    environment = verifier.expected_environment()
    environment["PG_SECRET_MARKER"] = "not-output"

    with pytest.raises(verifier.VerificationError) as caught:
        verifier.validate_environment(environment)

    rendered = verifier.canonical_json(verifier.failure_receipt(caught.value))
    assert "PG_SECRET_MARKER" not in rendered
    assert "PG*" in rendered


def test_pythonunbuffered_is_the_only_allowed_python_runtime_tuning() -> None:
    environment = verifier.expected_environment()
    environment["PYTHONUNBUFFERED"] = "1"

    assert verifier.validate_environment(environment) == tuple(sorted(verifier.expected_environment()))


def test_bash_env_executes_before_a_script_but_effective_verifier_rejects_it(tmp_path: Path) -> None:
    marker = "BASH_ENV_EXECUTED"
    payload = tmp_path / "bash-env.sh"
    payload.write_text(f"printf '{marker}\\n'\n", encoding="utf-8")
    completed = subprocess.run(
        ["/bin/bash", "-c", ":"],
        env={"BASH_ENV": str(payload), "PATH": verifier.RUNTIME_PATH},
        check=False,
        capture_output=True,
        text=True,
    )
    assert completed.returncode == 0
    assert completed.stdout.strip() == marker

    environment = verifier.expected_environment()
    environment["BASH_ENV"] = str(payload)
    with pytest.raises(verifier.VerificationError) as caught:
        verifier.validate_environment(environment)
    assert caught.value.fields == ("BASH_ENV",)


def test_admin_secret_reference_in_any_value_fails_without_field_or_value_leak() -> None:
    environment = verifier.expected_environment()
    environment["UNRELATED"] = f"prefix/{verifier.ADMINISTRATOR_PASSWORD_SECRET}/suffix"

    with pytest.raises(verifier.VerificationError) as caught:
        verifier.validate_environment(environment)

    rendered = verifier.canonical_json(verifier.failure_receipt(caught.value))
    assert caught.value.code == "administrator_secret_reference"
    assert verifier.ADMINISTRATOR_PASSWORD_SECRET not in rendered
    assert "UNRELATED" not in rendered


@pytest.mark.parametrize(
    "raw",
    [
        b"A=1",
        b"A=1\0\0",
        b"A=1\0BROKEN\0",
        b"=value\0",
        b"A=1\0A=2\0",
        b"A=\xff\0",
        b"",
    ],
)
def test_malformed_nul_environment_is_rejected(raw: bytes) -> None:
    with pytest.raises(verifier.VerificationError) as caught:
        verifier.parse_environment(raw)

    assert caught.value.code == "environment_malformed"


def test_parser_preserves_equals_inside_values() -> None:
    assert verifier.parse_environment(b"A=one=two\0B=\0") == {"A": "one=two", "B": ""}


@pytest.mark.parametrize("value", [0, -1, 1.5, "0", "01", "+1", "-3", "not-a-pid", True])
def test_pid_must_be_a_positive_integer(value: object) -> None:
    with pytest.raises(verifier.VerificationError) as caught:
        verifier.validate_pid(value)  # type: ignore[arg-type]

    assert caught.value.code == "invalid_pid"


def test_cli_errors_are_canonical_sanitized_json_and_exit_one(capsys: pytest.CaptureFixture[str]) -> None:
    secret_input = "not-a-pid-secret-marker"

    returncode = verifier.main(["--pid", secret_input])

    output = capsys.readouterr()
    assert returncode == 1
    assert output.err == ""
    assert json.loads(output.out) == {"error": {"code": "invalid_pid"}, "status": "fail"}
    assert secret_input not in output.out


@pytest.mark.skipif(not sys.platform.startswith("linux"), reason="requires Linux /proc/PID/environ")
def test_cli_reads_real_process_environment_and_detects_effective_conflict(
    capsys: pytest.CaptureFixture[str],
) -> None:
    expected = verifier.expected_environment()
    good_process = subprocess.Popen(
        ["/bin/sleep", "30"],
        env=expected,
        stdin=subprocess.DEVNULL,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )
    try:
        wait_for_process_environment(good_process, expected)
        assert verifier.main(["--pid", str(good_process.pid)]) == 0
        good_output = capsys.readouterr()
        assert good_output.err == ""
        assert json.loads(good_output.out)["status"] == "pass"
    finally:
        good_process.terminate()
        good_process.wait(timeout=5)

    conflicting = dict(expected)
    conflicting["TELEO_CLOUDSQL_USER"] = "postgres"
    conflicting["GOOGLE_APPLICATION_CREDENTIALS"] = "/tmp/broader-credential.json"
    bad_process = subprocess.Popen(
        ["/bin/sleep", "30"],
        env=conflicting,
        stdin=subprocess.DEVNULL,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )
    try:
        wait_for_process_environment(bad_process, conflicting)
        assert verifier.main(["--pid", str(bad_process.pid)]) == 1
        bad_output = capsys.readouterr()
        receipt = json.loads(bad_output.out)
        assert bad_output.err == ""
        assert receipt == {
            "error": {"code": "forbidden_environment_fields", "fields": ["GOOGLE_APPLICATION_CREDENTIALS"]},
            "status": "fail",
        }
        assert "postgres" not in bad_output.out
        assert "/tmp/broader-credential.json" not in bad_output.out
    finally:
        bad_process.terminate()
        bad_process.wait(timeout=5)


def test_unexpected_read_error_is_generic_and_does_not_echo_path(
    monkeypatch: pytest.MonkeyPatch,
    capsys: pytest.CaptureFixture[str],
) -> None:
    sensitive_marker = "sensitive-proc-read-detail"

    def fail_read(_self: object) -> bytes:
        raise OSError(sensitive_marker)

    monkeypatch.setattr(verifier.Path, "read_bytes", fail_read)

    assert verifier.main(["--pid", str(os.getpid())]) == 1
    output = capsys.readouterr()
    assert json.loads(output.out) == {"error": {"code": "internal_error"}, "status": "fail"}
    assert sensitive_marker not in output.out
