"""
Parser unit tests

Tests the Cedar text → PolicyRule conversion using the official Cedar engine.
These tests demonstrate how a client like highflame-authz would use the parser.
"""

import pytest
from highflame_policy import parse_cedar_to_rules, ParseResult


def test_parse_simple_permit():
    """Test parsing a simple permit policy."""
    cedar_text = '''
        @id("allow-read-files")
        permit(
            principal is User,
            action == Action::"read_file",
            resource is FilePath
        );
    '''

    result = parse_cedar_to_rules(cedar_text)

    assert len(result.errors) == 0
    assert len(result.rules) == 1
    assert len(result.unstructured) == 0

    rule = result.rules[0]
    assert rule["annotations"]["id"] == "allow-read-files"
    assert rule["effect"] == "permit"
    assert rule["principal"]["type"] == "User"
    assert rule["action"] == "read_file"
    assert rule["resource"]["type"] == "FilePath"
    assert rule["enabled"] is True


def test_parse_with_conditions():
    """Test parsing a policy with when conditions."""
    cedar_text = '''
        @id("block-high-risk")
        forbid(
            principal,
            action == Action::"execute_tool",
            resource
        )
        when {
            context.threat_level == "high"
        };
    '''

    result = parse_cedar_to_rules(cedar_text)

    assert len(result.errors) == 0
    assert len(result.rules) == 1

    rule = result.rules[0]
    assert rule["annotations"]["id"] == "block-high-risk"
    assert rule["effect"] == "forbid"
    assert rule["action"] == "execute_tool"

    # Check condition was parsed (either as structured or raw)
    conditions = rule.get("conditions", [])
    raw_condition = rule.get("rawCondition")

    if len(conditions) > 0:
        assert conditions[0]["field"] == "threat_level"
        assert conditions[0]["operator"] == "eq"
        assert conditions[0]["value"] == "high"
    else:
        assert raw_condition is not None


def test_parse_invalid_syntax():
    """Test that invalid Cedar syntax returns errors."""
    invalid_cedar = '''
        permit(
            principal is User
            // missing comma and rest of policy
    '''

    result = parse_cedar_to_rules(invalid_cedar)

    assert len(result.errors) > 0


def test_parse_multiple_policies():
    """Test parsing multiple policies."""
    cedar_text = '''
        @id("rule-1")
        permit(principal, action == Action::"read", resource);

        @id("rule-2")
        forbid(principal, action == Action::"delete", resource);
    '''

    result = parse_cedar_to_rules(cedar_text)

    assert len(result.errors) == 0
    assert len(result.rules) == 2
    assert result.rules[0]["effect"] == "permit"
    assert result.rules[1]["effect"] == "forbid"


def test_multi_policy_raw_condition_extracts_correct_when_clause():
    """Test that rawCondition extracts the correct when clause in multi-policy text.

    When multiple policies exist and a complex condition can't be mapped to
    structured format, the parser should extract the when clause from the
    correct policy (located by @id annotation), not the first one found.
    """
    cedar_text = '''
        @id("simple-policy")
        forbid(
            principal,
            action == Action::"read_file",
            resource
        )
        when { context.threat_count > 0 };

        @id("complex-policy")
        forbid(
            principal,
            action == Action::"call_tool",
            resource
        )
        when { context.yara_threats.containsAny(context.blocked_threats) };
    '''

    result = parse_cedar_to_rules(cedar_text)

    assert len(result.errors) == 0

    # Find the complex policy
    complex_rule = None
    for rule in result.rules:
        if rule["annotations"]["id"] == "complex-policy":
            complex_rule = rule
            break

    assert complex_rule is not None

    # The rawCondition should be from the complex policy's when clause,
    # NOT from the simple policy's when clause
    raw = complex_rule.get("rawCondition")
    if raw:
        assert "containsAny" in raw
        assert "threat_count > 0" not in raw


def test_round_trip_remove_and_readd_rule():
    """Round-trip test: parse → remove rule → rebuild → re-parse → add back → verify.

    Reads shared Cedar fixture (sorted by ID, with Overwatch namespaces).
    Contains both simple conditions (structured PolicyCondition) and complex
    conditions (rawCondition). Verifies that removing a rule, reconstructing
    Cedar, re-parsing, adding it back, and reconstructing again produces Cedar
    text identical to the original input.
    """
    from pathlib import Path
    from highflame_policy import rules_to_cedar

    # Read shared test fixture (builder output format, sorted by @id)
    fixture_path = Path(__file__).parent.parent.parent.parent / "test-fixtures" / "round-trip.cedar"
    original_cedar = fixture_path.read_text().rstrip("\n")

    # --- Step 1: Parse original ---
    original_result = parse_cedar_to_rules(original_cedar)
    assert len(original_result.errors) == 0, f"Parse errors: {original_result.errors}"
    assert len(original_result.rules) == 4

    # Verify we have both simple (structured) and complex (raw) conditions
    simple_rule = _find_rule("simple-block-threats", original_result.rules)
    complex_rule = _find_rule("complex-block-injection", original_result.rules)
    assert simple_rule is not None
    assert complex_rule is not None
    assert len(simple_rule.get("conditions", [])) > 0, "Simple rule should have structured conditions"
    assert complex_rule.get("rawCondition") is not None, "Complex rule should have rawCondition"

    # --- Step 2: Remove complex rule → rebuild → re-parse ---
    remaining = [r for r in original_result.rules if r["annotations"]["id"] != "complex-block-injection"]
    removed_rule = complex_rule
    assert len(remaining) == 3

    partial_cedar = rules_to_cedar(remaining)
    partial_result = parse_cedar_to_rules(partial_cedar)
    assert len(partial_result.errors) == 0, f"Partial parse errors: {partial_result.errors}"
    assert len(partial_result.rules) == 3

    # Verify each remaining rule is preserved through the round-trip
    for orig in remaining:
        reparsed = _find_rule(orig["annotations"]["id"], partial_result.rules)
        assert reparsed is not None, f"Missing rule after rebuild: {orig['annotations']['id']}"
        _assert_rules_equivalent(orig, reparsed)

    # --- Step 3: Add back → rebuild → re-parse ---
    all_rules = partial_result.rules + [removed_rule]
    final_cedar = rules_to_cedar(all_rules)
    final_result = parse_cedar_to_rules(final_cedar)
    assert len(final_result.errors) == 0, f"Final parse errors: {final_result.errors}"
    assert len(final_result.rules) == 4

    # Verify ALL rules match the original parsed rules
    for orig in original_result.rules:
        final = _find_rule(orig["annotations"]["id"], final_result.rules)
        assert final is not None, f"Missing rule in final: {orig['annotations']['id']}"
        _assert_rules_equivalent(orig, final)

    # --- Step 4: Verify parse→build cycle is idempotent ---
    # The engine may normalize rawCondition text (e.g., adding parens), so we
    # compare against canonical form (first parse→build) rather than original fixture.
    original_sorted = sorted(original_result.rules, key=lambda r: r["annotations"]["id"])
    for i, r in enumerate(original_sorted):
        r["order"] = i
    canonical_cedar = rules_to_cedar(original_sorted)

    final_sorted = sorted(final_result.rules, key=lambda r: r["annotations"]["id"])
    for i, r in enumerate(final_sorted):
        r["order"] = i
    final_reconstructed = rules_to_cedar(final_sorted)
    assert final_reconstructed == canonical_cedar, \
        f"Final Cedar text should match canonical form.\nExpected:\n{canonical_cedar}\nGot:\n{final_reconstructed}"


def _find_rule(rule_id, rules):
    """Find a rule by its annotations.id."""
    return next((r for r in rules if r.get("annotations", {}).get("id") == rule_id), None)


def _assert_rules_equivalent(a, b):
    """Assert two PolicyRules are semantically equivalent after round-trip."""
    assert a["annotations"]["id"] == b["annotations"]["id"], \
        f"ID mismatch: {a['annotations']['id']} != {b['annotations']['id']}"
    assert a["annotations"]["name"] == b["annotations"]["name"], \
        f"Name mismatch for {a['annotations']['id']}"
    assert a["effect"] == b["effect"], \
        f"Effect mismatch for {a['annotations']['id']}"
    assert a.get("action") == b.get("action"), \
        f"Action mismatch for {a['annotations']['id']}: {a.get('action')} != {b.get('action')}"
    assert a.get("principal") == b.get("principal"), \
        f"Principal mismatch for {a['annotations']['id']}"
    assert a.get("resource") == b.get("resource"), \
        f"Resource mismatch for {a['annotations']['id']}"
    assert a.get("conditions", []) == b.get("conditions", []), \
        f"Conditions mismatch for {a['annotations']['id']}"
    assert a.get("rawCondition") == b.get("rawCondition"), \
        f"rawCondition mismatch for {a['annotations']['id']}: {a.get('rawCondition')!r} != {b.get('rawCondition')!r}"


def test_unless_clause_in_unstructured():
    """Test that policies with unless clauses go to unstructured."""
    cedar_text = '''
        permit(principal, action, resource)
        unless {
            context.is_blocked == true
        };
    '''

    result = parse_cedar_to_rules(cedar_text)

    # Unless clauses can't be represented as PolicyRule
    assert len(result.rules) == 0
    assert len(result.unstructured) > 0


def test_parse_condition_expression():
    """Parse Cedar with has && gte and verify conditionExpression tree."""
    cedar_text = '''
        @id("block-injection")
        forbid(
            principal,
            action == Action::"process_prompt",
            resource
        )
        when {
            context has injection_confidence && context.injection_confidence >= 75
        };
    '''

    result = parse_cedar_to_rules(cedar_text)

    assert len(result.errors) == 0
    assert len(result.rules) == 1

    rule = result.rules[0]
    assert rule["annotations"]["id"] == "block-injection"

    # Verify conditionExpression tree was populated
    expr = rule.get("conditionExpression")
    assert expr is not None
    assert expr["kind"] == "and"
    assert len(expr["children"]) == 2

    # First child: has
    assert expr["children"][0]["kind"] == "has"
    assert expr["children"][0]["field"] == "injection_confidence"

    # Second child: comparison
    assert expr["children"][1]["kind"] == "comparison"
    assert expr["children"][1]["field"] == "injection_confidence"
    assert expr["children"][1]["operator"] == "gte"
    assert expr["children"][1]["value"] == 75
