"""Tests for all improvement plan features."""

from __future__ import annotations

import textwrap

import pytest

from acl_engine.engine import ACLEngine
from acl_engine.parser import ACLParseError, parse_acl
from acl_engine.policy import ExecutionPolicy


# ---------------------------------------------------------------------------
# 1. Multiline parameter support
# ---------------------------------------------------------------------------

def test_parser_supports_multiline_params(tmp_path) -> None:
    acl_text = textwrap.dedent("""\
        file.write(
            path="hello.txt",
            content="multiline params"
        ) -> result
    """)
    engine = ACLEngine(workspace=tmp_path)
    result = engine.execute(acl_text)

    assert result["status"] == "success"
    assert (tmp_path / "hello.txt").read_text() == "multiline params"


# ---------------------------------------------------------------------------
# 2. Triple-quoted strings
# ---------------------------------------------------------------------------

def test_parser_supports_triple_quoted_strings(tmp_path) -> None:
    acl_text = 'file.write(path="multi.txt", content="""line1\nline2\nline3""")'
    engine = ACLEngine(workspace=tmp_path)
    result = engine.execute(acl_text)

    assert result["status"] == "success"
    assert (tmp_path / "multi.txt").read_text() == "line1\nline2\nline3"


def test_parser_triple_string_with_literal_newlines(tmp_path) -> None:
    acl_text = 'file.write(path="raw.txt", content="""hello\nworld""")'
    engine = ACLEngine(workspace=tmp_path)
    result = engine.execute(acl_text)

    assert result["status"] == "success"
    content = (tmp_path / "raw.txt").read_text()
    assert "hello" in content and "world" in content


def test_parser_rejects_unterminated_triple_string() -> None:
    with pytest.raises(ACLParseError, match="unclosed parameter list"):
        parse_acl('util.echo(value="""not closed)')


# ---------------------------------------------------------------------------
# 3. Capability-based permissions
# ---------------------------------------------------------------------------

def test_capability_enforcement_blocks_denied_action(tmp_path) -> None:
    engine = ACLEngine(
        workspace=tmp_path,
        allowed_capabilities={"memory", "utility"},
    )
    result = engine.execute('file.read(path="test.txt")')

    assert result["status"] == "error"
    assert result["error"]["code"] == "safety_error"
    assert "denied capabilities" in result["error"]["message"]
    assert "filesystem" in result["error"]["message"]


def test_capability_enforcement_allows_permitted_actions(tmp_path) -> None:
    engine = ACLEngine(
        workspace=tmp_path,
        allowed_capabilities={"utility", "memory"},
    )
    result = engine.execute('util.echo(value="hello") -> msg')

    assert result["status"] == "success"
    assert result["data"]["msg"] == "hello"


def test_strict_policy_blocks_network_capability(tmp_path) -> None:
    engine = ACLEngine(workspace=tmp_path, policy="strict")
    result = engine.execute('net.request(method="GET", url="https://example.com")')

    assert result["status"] == "error"
    assert result["error"]["code"] == "safety_error"


def test_strict_policy_blocks_process_capability(tmp_path) -> None:
    engine = ACLEngine(workspace=tmp_path, policy="strict")
    result = engine.execute('system.run(command="ls")')

    assert result["status"] == "error"
    assert result["error"]["code"] == "safety_error"


def test_strict_policy_allows_filesystem_capability(tmp_path) -> None:
    engine = ACLEngine(workspace=tmp_path, policy="strict")
    (tmp_path / "test.txt").write_text("hello")
    result = engine.execute('file.read(path="test.txt") -> data')

    assert result["status"] == "success"
    assert result["data"]["data"] == "hello"


# ---------------------------------------------------------------------------
# 4. Value constraint validation
# ---------------------------------------------------------------------------

def test_constraint_min_length_blocks_empty_string(tmp_path) -> None:
    engine = ACLEngine(workspace=tmp_path)
    result = engine.execute('file.read(path="")')

    assert result["status"] == "error"
    assert "min" in result["error"]["message"].lower()


def test_constraint_min_value_blocks_negative_sleep(tmp_path) -> None:
    engine = ACLEngine(workspace=tmp_path)
    result = engine.execute('util.sleep(seconds=-1)')

    assert result["status"] == "error"


def test_custom_action_with_constraints(tmp_path) -> None:
    engine = ACLEngine(workspace=tmp_path)
    engine.register_action(
        "test.validate",
        lambda value, **_: value,
        {"value": {"type": str, "min_length": 3, "max_length": 10}},
        capabilities={"utility"},
    )

    ok_result = engine.execute('test.validate(value="hello") -> out')
    assert ok_result["status"] == "success"

    short_result = engine.execute('test.validate(value="ab") -> out')
    assert short_result["status"] == "error"
    assert "min" in short_result["error"]["message"].lower()


# ---------------------------------------------------------------------------
# 5. Raw vs resolved params in logs
# ---------------------------------------------------------------------------

def test_logs_contain_raw_and_resolved_params(tmp_path) -> None:
    engine = ACLEngine(workspace=tmp_path)
    acl_text = textwrap.dedent("""\
        util.echo(value="hello") -> msg
        util.echo(value=msg) -> out
    """)
    result = engine.execute(acl_text)

    assert result["status"] == "success"
    first_log = result["logs"][0]
    assert "raw_params" in first_log
    assert "resolved_params" in first_log

    second_log = result["logs"][1]
    assert second_log["raw_params"]["value"] == {"variable": "msg"}
    assert second_log["resolved_params"]["value"] == "hello"


# ---------------------------------------------------------------------------
# 6. Standardized output contract
# ---------------------------------------------------------------------------

def test_output_contract_always_has_error_field(tmp_path) -> None:
    engine = ACLEngine(workspace=tmp_path)
    result = engine.execute('util.echo(value="hi")')

    assert "error" in result
    assert result["error"] is None
    assert "acl_version" in result
    assert "output_version" in result
    assert "trace_id" in result
    assert "data" in result
    assert "logs" in result
    assert "summary" in result


def test_output_contract_error_case(tmp_path) -> None:
    engine = ACLEngine(workspace=tmp_path)
    result = engine.execute('file.read(path="../../etc/passwd")')

    assert result["error"] is not None
    assert "type" in result["error"]
    assert "message" in result["error"]
    assert "code" in result["error"]


# ---------------------------------------------------------------------------
# 7. Policy from_dict
# ---------------------------------------------------------------------------

def test_policy_from_dict() -> None:
    policy = ExecutionPolicy.from_dict({
        "name": "custom-test",
        "allowed_commands": ["python"],
        "blocked_patterns": ["rm"],
        "allow_network": False,
        "allow_delete": False,
        "allowed_capabilities": ["utility", "memory"],
    })

    assert policy.name == "custom-test"
    assert "python" in policy.allowed_commands
    assert policy.allow_network is False
    assert policy.allowed_capabilities == frozenset({"utility", "memory"})


def test_policy_from_dict_defaults() -> None:
    policy = ExecutionPolicy.from_dict({})

    assert policy.name == "custom"
    assert policy.allow_network is True
    assert policy.allow_delete is True
    assert policy.allowed_capabilities is None
    assert policy.allowed_domains is None


def test_policy_from_dict_used_by_engine(tmp_path) -> None:
    custom = ExecutionPolicy.from_dict({
        "name": "lockdown",
        "allowed_commands": [],
        "blocked_patterns": [],
        "allow_network": False,
        "allowed_capabilities": ["utility"],
    })
    engine = ACLEngine(workspace=tmp_path, policy=custom)
    result = engine.execute('util.echo(value="ok") -> out')

    assert result["status"] == "success"


def test_unknown_policy_name_returns_clear_error() -> None:
    with pytest.raises(ValueError, match="unknown policy"):
        ACLEngine(policy="missing-policy")


# ---------------------------------------------------------------------------
# 8. Upgraded register_action API
# ---------------------------------------------------------------------------

def test_register_action_accepts_full_schema(tmp_path) -> None:
    engine = ACLEngine(workspace=tmp_path)
    engine.register_action(
        "custom.greet",
        lambda name, **_: f"hello {name}",
        {"name": {"type": str, "description": "Name to greet", "min_length": 1}},
        capabilities={"utility"},
        description="Greet a person",
        returns="string",
    )

    schema = engine.describe_actions()
    assert "custom.greet" in schema
    assert schema["custom.greet"]["description"] == "Greet a person"
    assert schema["custom.greet"]["capabilities"] == ["utility"]
    assert schema["custom.greet"]["params"]["name"]["min_length"] == 1


# ---------------------------------------------------------------------------
# 9. Trace ID
# ---------------------------------------------------------------------------

def test_trace_id_in_output(tmp_path) -> None:
    engine = ACLEngine(workspace=tmp_path, trace_id="test-trace-42")
    result = engine.execute('util.echo(value="x")')

    assert result["trace_id"] == "test-trace-42"


def test_trace_id_auto_generated(tmp_path) -> None:
    engine = ACLEngine(workspace=tmp_path)
    result = engine.execute('util.echo(value="x")')

    assert result["trace_id"] is not None
    assert len(result["trace_id"]) > 0


# ---------------------------------------------------------------------------
# 10. Error taxonomy
# ---------------------------------------------------------------------------

def test_error_taxonomy_parse_error() -> None:
    from acl_engine.parser import ACLParseError
    e = ACLParseError("test", line_number=5, column=3)
    assert e.code == "parse_error"
    assert e.line_number == 5
    assert e.column == 3


def test_error_taxonomy_validation_error() -> None:
    from acl_engine.validator import ACLValidationError
    e = ACLValidationError("bad param", action="file.read", line_number=2)
    assert e.code == "validation_error"
    assert e.action == "file.read"


def test_error_taxonomy_execution_error() -> None:
    from acl_engine.executor import ACLExecutionError, ACLSafetyError, ACLTimeoutError

    e = ACLExecutionError("failed", action="file.read", retryable=False)
    assert e.code == "execution_error"

    s = ACLSafetyError("blocked", action="file.read")
    assert s.code == "safety_error"

    t = ACLTimeoutError("timeout", action="system.run")
    assert t.code == "timeout_error"
    assert t.retryable is True


def test_error_taxonomy_variable_error() -> None:
    from acl_engine.context import ACLVariableError
    e = ACLVariableError("myvar")
    assert e.code == "variable_error"
    assert e.name == "myvar"


# ---------------------------------------------------------------------------
# 11. Describe policy includes capabilities
# ---------------------------------------------------------------------------

def test_describe_policy_includes_capabilities(tmp_path) -> None:
    engine = ACLEngine(workspace=tmp_path, policy="strict")
    desc = engine.describe_policy()

    assert "allowed_capabilities" in desc
    assert "filesystem" in desc["allowed_capabilities"]
    assert "network" not in desc["allowed_capabilities"]


# ---------------------------------------------------------------------------
# 12. Plugin module loading
# ---------------------------------------------------------------------------

def test_load_plugin_module(tmp_path) -> None:
    plugin_file = tmp_path / "my_plugin.py"
    plugin_file.write_text(textwrap.dedent("""\
        def _greet(name, **kwargs):
            return f"hi {name}"

        def register(registry):
            registry.register(
                "plugin.greet",
                _greet,
                {"name": {"type": str, "description": "Name"}},
                capabilities={"utility"},
                description="Greet via plugin",
                returns="string",
            )
    """))

    engine = ACLEngine(workspace=tmp_path)
    engine.load_plugin_module(plugin_file)

    assert "plugin.greet" in engine.list_actions()
    result = engine.execute('plugin.greet(name="World") -> msg')
    assert result["status"] == "success"
    assert result["data"]["msg"] == "hi World"


def test_load_plugin_module_missing_register(tmp_path) -> None:
    bad_plugin = tmp_path / "bad_plugin.py"
    bad_plugin.write_text("x = 1\n")

    engine = ACLEngine(workspace=tmp_path)
    with pytest.raises(ImportError, match="no register"):
        engine.load_plugin_module(bad_plugin)


# ---------------------------------------------------------------------------
# 13. Execution guarantees — sequential + stop_on_error
# ---------------------------------------------------------------------------

def test_execution_is_sequential(tmp_path) -> None:
    engine = ACLEngine(workspace=tmp_path)
    acl_text = textwrap.dedent("""\
        util.echo(value="a") -> x
        util.echo(value="b") -> y
        util.echo(value="c") -> z
    """)
    result = engine.execute(acl_text)

    assert result["status"] == "success"
    assert [log["action"] for log in result["logs"]] == [
        "util.echo", "util.echo", "util.echo"
    ]
    steps = [log["step"] for log in result["logs"]]
    assert steps == [1, 2, 3]


def test_stop_on_error_configurable(tmp_path) -> None:
    engine = ACLEngine(workspace=tmp_path, stop_on_error=False)
    acl_text = textwrap.dedent("""\
        util.echo(value="before") -> a
        file.read(path="nonexistent.txt") -> b
        util.echo(value="after") -> c
    """)
    result = engine.execute(acl_text)

    assert result["status"] == "error"
    assert len(result["logs"]) == 3
    assert result["data"]["a"] == "before"
    assert result["data"]["c"] == "after"


# ---------------------------------------------------------------------------
# 14. Output versioning
# ---------------------------------------------------------------------------

def test_output_version_is_1_2(tmp_path) -> None:
    engine = ACLEngine(workspace=tmp_path)
    result = engine.execute('util.echo(value="x")')

    assert result["acl_version"] == "1.0"
    assert result["output_version"] == "1.2"
