from __future__ import annotations

import json
from importlib import metadata as importlib_metadata
from pathlib import Path

import pytest

from icsd import __version__
from icsd.cli.main import (
    CLI_PROG_NAME,
    DEFAULT_DEMO_OUTPUT,
    EXIT_FINDINGS,
    EXIT_OK,
    EXIT_TOOL_ERROR,
    LINT_BASELINE_PROFILE,
    LINT_POLICY_PROFILE,
    build_parser,
    main,
)
from icsd.core.evidence import EvidenceBundle, validate_evidence_payload


def test_main_without_command_returns_tool_error() -> None:
    assert main([]) == EXIT_TOOL_ERROR


def test_build_parser_exposes_expected_v1_commands_and_lint_defaults() -> None:
    parser = build_parser()

    demo_args = parser.parse_args(["demo"])
    demo_custom_args = parser.parse_args(["demo", "custom.json"])
    verify_args = parser.parse_args(["verify", "artifact.json"])
    verify_json_args = parser.parse_args(["verify", "artifact.json", "--format", "json"])
    lint_args = parser.parse_args(["lint", "."])
    lint_policy_args = parser.parse_args(
        ["lint", ".", "--profile", LINT_POLICY_PROFILE, "--format", "json"]
    )

    assert parser.prog == CLI_PROG_NAME
    assert demo_args.command == "demo"
    assert demo_args.output == DEFAULT_DEMO_OUTPUT
    assert demo_custom_args.output == "custom.json"
    assert verify_args.command == "verify"
    assert verify_args.evidence_json == "artifact.json"
    assert verify_args.output_format == "text"
    assert verify_json_args.output_format == "json"
    assert lint_args.command == "lint"
    assert lint_args.profile == LINT_BASELINE_PROFILE
    assert lint_args.output_format == "text"
    assert lint_policy_args.profile == LINT_POLICY_PROFILE
    assert lint_policy_args.output_format == "json"


def test_build_parser_help_describes_verify_as_evidence_envelope_validation() -> None:
    parser = build_parser()

    assert "Validate evidence envelope schema and hash fields." in parser.format_help()


def test_verify_subcommand_help_describes_evidence_json_envelope() -> None:
    parser = build_parser()
    subparsers_action = next(
        action for action in parser._subparsers._group_actions if hasattr(action, "choices")
    )
    verify_parser = subparsers_action.choices["verify"]

    assert "Path to evidence JSON envelope." in verify_parser.format_help()


def test_main_unknown_command_returns_argparse_error_exit_code() -> None:
    with pytest.raises(SystemExit) as exc_info:
        main(["unknown"])

    assert exc_info.value.code == EXIT_TOOL_ERROR


def test_main_verify_valid_bundle_returns_ok(tmp_path: Path) -> None:
    bundle = EvidenceBundle.from_states(
        transition_id="89f93f18-5de2-4177-9149-7a8f7ecb4f71",
        entity_type="order",
        entity_id="ord-100",
        actor="cli-test",
        authority="pytest",
        before_state={"status": "draft"},
        after_state={"status": "approved"},
        timestamp="2026-03-03T00:00:00Z",
    )
    evidence_path = tmp_path / "evidence.json"
    evidence_path.write_text(json.dumps(bundle.as_dict()), encoding="utf-8")

    assert main(["verify", str(evidence_path)]) == EXIT_OK


def test_main_verify_json_output_reports_ok(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    bundle = EvidenceBundle.from_states(
        transition_id="89f93f18-5de2-4177-9149-7a8f7ecb4f71",
        entity_type="order",
        entity_id="ord-100",
        actor="cli-test",
        authority="pytest",
        before_state={"status": "draft"},
        after_state={"status": "approved"},
        timestamp="2026-03-03T00:00:00Z",
    )
    evidence_path = tmp_path / "evidence.json"
    evidence_path.write_text(json.dumps(bundle.as_dict()), encoding="utf-8")

    exit_code = main(["verify", str(evidence_path), "--format", "json"])
    report = json.loads(capsys.readouterr().out)

    assert exit_code == EXIT_OK
    assert report == {
        "command": "verify",
        "path": str(evidence_path),
        "status": "ok",
        "exit_code": EXIT_OK,
        "error_count": 0,
        "errors": [],
    }


def test_main_verify_invalid_bundle_returns_tool_error(
    tmp_path: Path,
) -> None:
    evidence_path = tmp_path / "evidence.json"
    evidence_path.write_text(
        json.dumps({"schema_version": "0.1", "transition_id": "not-a-uuid"}),
        encoding="utf-8",
    )

    assert main(["verify", str(evidence_path)]) == EXIT_TOOL_ERROR


def test_main_verify_json_output_reports_stable_error_order(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    evidence_path = tmp_path / "evidence.json"
    evidence_path.write_text(
        json.dumps(
            {
                "schema_version": "9.9",
                "transition_id": "not-a-uuid",
                "entity_type": "",
                "entity_id": "acct-001",
                "actor": "",
                "authority": "policy/account-update",
                "timestamp": "2026-03-03T09:30:00+01:00",
                "before_hash": "bad-hash",
                "after_hash": "BAD-HASH",
            }
        ),
        encoding="utf-8",
    )

    exit_code = main(["verify", str(evidence_path), "--format", "json"])
    report = json.loads(capsys.readouterr().out)

    assert exit_code == EXIT_TOOL_ERROR
    assert report["status"] == "error"
    assert report["error_count"] == 7
    assert report["errors"] == [
        "Unsupported evidence schema version: '9.9'.",
        "transition_id must be a valid UUID string; got 'not-a-uuid'.",
        "entity_type must be a non-empty string.",
        "actor must be a non-empty string.",
        "timestamp must include a UTC offset; got '2026-03-03T09:30:00+01:00'.",
        "before_hash must be a lowercase hexadecimal sha256 digest.",
        "after_hash must be a lowercase hexadecimal sha256 digest.",
    ]


def test_main_verify_missing_file_returns_tool_error(tmp_path: Path) -> None:
    missing_path = tmp_path / "missing.json"

    assert main(["verify", str(missing_path)]) == EXIT_TOOL_ERROR


def test_main_verify_json_output_reports_missing_file(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    missing_path = tmp_path / "missing.json"

    exit_code = main(["verify", str(missing_path), "--format", "json"])
    report = json.loads(capsys.readouterr().out)

    assert exit_code == EXIT_TOOL_ERROR
    assert report == {
        "command": "verify",
        "path": str(missing_path),
        "status": "error",
        "exit_code": EXIT_TOOL_ERROR,
        "error_count": 1,
        "errors": [f"path does not exist: {missing_path}."],
    }


def test_main_verify_json_output_reports_non_file_path(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    exit_code = main(["verify", str(tmp_path), "--format", "json"])
    report = json.loads(capsys.readouterr().out)

    assert exit_code == EXIT_TOOL_ERROR
    assert report == {
        "command": "verify",
        "path": str(tmp_path),
        "status": "error",
        "exit_code": EXIT_TOOL_ERROR,
        "error_count": 1,
        "errors": [f"path is not a file: {tmp_path}."],
    }


def test_main_verify_non_object_payload_returns_tool_error(tmp_path: Path) -> None:
    evidence_path = tmp_path / "evidence.json"
    evidence_path.write_text('["not-an-object"]', encoding="utf-8")

    assert main(["verify", str(evidence_path)]) == EXIT_TOOL_ERROR


def test_main_demo_writes_default_output_in_current_directory(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.chdir(tmp_path)

    assert main(["demo"]) == EXIT_OK
    assert (tmp_path / DEFAULT_DEMO_OUTPUT).is_file()


def test_main_demo_writes_deterministic_valid_bundle(
    tmp_path: Path,
) -> None:
    first_path = tmp_path / "first.evidence.json"
    second_path = tmp_path / "second.evidence.json"

    assert main(["demo", str(first_path)]) == EXIT_OK
    assert main(["demo", str(second_path)]) == EXIT_OK

    first_text = first_path.read_text(encoding="utf-8")
    second_text = second_path.read_text(encoding="utf-8")
    payload = json.loads(first_text)

    assert first_text == second_text
    assert validate_evidence_payload(payload) == ()
    assert main(["verify", str(first_path)]) == EXIT_OK


def test_main_lint_clean_tree_returns_ok(tmp_path: Path) -> None:
    (tmp_path / "ok.py").write_text("def hello() -> str:\n    return 'ok'\n", encoding="utf-8")
    (tmp_path / "ok.json").write_text('{"hello": "world"}\n', encoding="utf-8")

    assert main(["lint", str(tmp_path)]) == EXIT_OK


def test_main_lint_directory_skips_virtualenv_and_site_packages_trees(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    (tmp_path / "ok.py").write_text("def hello() -> str:\n    return 'ok'\n", encoding="utf-8")
    venv_fixture = (
        tmp_path
        / ".venv"
        / "Lib"
        / "site-packages"
        / "joblib"
        / "test"
        / "test_func_inspect_special_encoding.py"
    )
    venv_fixture.parent.mkdir(parents=True)
    venv_fixture.write_bytes(b"# cp1252 fixture without cookie\nvalue = '\xa4'\n")

    exit_code = main(["lint", str(tmp_path), "--format", "json"])
    report = json.loads(capsys.readouterr().out)

    assert exit_code == EXIT_OK
    assert report["status"] == "ok"
    assert report["finding_count"] == 0


def test_main_lint_python_file_honours_declared_source_encoding(tmp_path: Path) -> None:
    encoded_source = "# -*- coding: cp1252 -*-\nvalue = '\xa4'\n"
    (tmp_path / "encoded.py").write_bytes(encoded_source.encode("cp1252"))

    assert main(["lint", str(tmp_path)]) == EXIT_OK


def test_main_lint_python_syntax_error_returns_findings(
    tmp_path: Path,
) -> None:
    (tmp_path / "broken.py").write_text("def broken(:\n    return 1\n", encoding="utf-8")

    assert main(["lint", str(tmp_path)]) == EXIT_FINDINGS


def test_main_lint_invalid_json_returns_findings(tmp_path: Path) -> None:
    (tmp_path / "broken.json").write_text("{broken}\n", encoding="utf-8")

    assert main(["lint", str(tmp_path)]) == EXIT_FINDINGS


def test_main_lint_invalid_utf8_json_returns_findings(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    (tmp_path / "broken.json").write_bytes(b'{"currency": "\xa4"}\n')

    exit_code = main(["lint", str(tmp_path), "--format", "json"])
    report = json.loads(capsys.readouterr().out)

    assert exit_code == EXIT_FINDINGS
    assert report["status"] == "findings"
    assert report["finding_count"] == 1
    assert report["findings"][0]["rule"] == "json-encoding"


def test_main_lint_missing_path_returns_tool_error(tmp_path: Path) -> None:
    missing_path = tmp_path / "missing"

    assert main(["lint", str(missing_path)]) == EXIT_TOOL_ERROR


def test_main_lint_json_output_reports_clean_tree(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    (tmp_path / "ok.py").write_text("def hello() -> str:\n    return 'ok'\n", encoding="utf-8")

    exit_code = main(["lint", str(tmp_path), "--format", "json"])
    output = capsys.readouterr().out.strip()
    report = json.loads(output)

    assert exit_code == EXIT_OK
    expected_output = (
        '{"command":"lint","exit_code":0,"finding_count":0,"findings":[],"path":'
        f'{json.dumps(str(tmp_path))},"profile":"baseline","status":"ok"}}'
    )
    assert output == expected_output
    assert report["status"] == "ok"
    assert report["profile"] == "baseline"
    assert report["finding_count"] == 0
    assert report["findings"] == []


def test_main_lint_json_output_reports_findings(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    (tmp_path / "broken.json").write_text("{broken}\n", encoding="utf-8")

    exit_code = main(["lint", str(tmp_path), "--format", "json"])
    report = json.loads(capsys.readouterr().out)

    assert exit_code == EXIT_FINDINGS
    assert report["status"] == "findings"
    assert report["finding_count"] == 1
    assert report["findings"][0]["rule"] == "json-syntax"


def test_main_lint_json_output_reports_tool_error(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    missing_path = tmp_path / "missing"

    exit_code = main(["lint", str(missing_path), "--format", "json"])
    report = json.loads(capsys.readouterr().out)

    assert exit_code == EXIT_TOOL_ERROR
    assert report["status"] == "error"
    assert report["error"] == f"path does not exist: {missing_path}."
    assert report["finding_count"] == 0


def test_main_lint_policy_profile_flags_dynamic_exec(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    (tmp_path / "policy.py").write_text("value = eval('1')\n", encoding="utf-8")

    exit_code = main(["lint", str(tmp_path), "--profile", "policy-v0.1", "--format", "json"])
    report = json.loads(capsys.readouterr().out)

    assert exit_code == EXIT_FINDINGS
    assert report["profile"] == "policy-v0.1"
    assert report["finding_count"] == 1
    assert report["findings"][0]["rule"] == "python-dynamic-exec"


def test_main_lint_policy_profile_validates_evidence_payload(
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    (tmp_path / "evidence.json").write_text(
        json.dumps({"schema_version": "0.1", "transition_id": "not-a-uuid"}),
        encoding="utf-8",
    )

    exit_code = main(["lint", str(tmp_path), "--profile", "policy-v0.1", "--format", "json"])
    report = json.loads(capsys.readouterr().out)

    assert exit_code == EXIT_FINDINGS
    assert report["profile"] == "policy-v0.1"
    assert any(finding["rule"] == "evidence-schema" for finding in report["findings"])


def test_main_version_outputs_package_version(capsys: pytest.CaptureFixture[str]) -> None:
    with pytest.raises(SystemExit) as exc_info:
        main(["--version"])

    assert exc_info.value.code == 0
    assert capsys.readouterr().out.strip() == f"{CLI_PROG_NAME} {__version__}"


def test_distribution_version_matches_runtime_version() -> None:
    try:
        installed_version = importlib_metadata.version("dpa-icsd")
    except importlib_metadata.PackageNotFoundError:
        pytest.skip("dpa-icsd distribution metadata is unavailable in this environment.")

    assert installed_version == __version__
