"""Executable semantic and adversarial tests for the native local evaluator."""

import copy
import importlib.util
import json
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest

ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location("odexa", ROOT / "odexa_ref" / "policy.py")
odexa = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(odexa)
NOW = "2026-09-16T12:00:00Z"
BASE = json.loads((ROOT / "examples" / "free-basic.json").read_text())


def policy():
    return copy.deepcopy(BASE)


def request(url="https://origin.example/licensed/about.txt", actions=None, purposes=None, support=None):
    return {"url": url, "actions": actions or ["retrieve"], "purposes": purposes or ["public_retrieval"], "supported_obligations": support or []}


def rule(rule_id, effect="permit", actions=None, purposes=None, duties=None):
    return {"id": rule_id, "resource_ids": ["clean-about"], "actions": actions or ["retrieve"], "purposes": purposes or ["public_retrieval"], "effect": effect, "obligations": duties or []}


class EvaluatorTests(unittest.TestCase):
    def check(self, p=None, r=None, expected="permit", now=NOW):
        result = odexa.evaluate(p if p is not None else policy(), r if r is not None else request(), now)
        self.assertEqual(result["decision"], expected, result)
        self.assertFalse(result["access_credential_verified"])
        return result

    def test_all_examples_validate(self):
        files = sorted((ROOT / "examples").glob("*.json"))
        self.assertEqual(len(files), 4)
        for path in files:
            odexa.validate_policy(odexa.load_policy(path))

    def test_no_implicit_storage(self):
        self.check(r=request(actions=["store"]), expected="no_grant")

    def test_no_implicit_training(self):
        self.check(r=request(purposes=["model_training"]), expected="no_grant")

    def test_no_implicit_redistribution(self):
        self.check(r=request(actions=["redistribute"]), expected="no_grant")

    def test_all_requested_purposes_checked(self):
        result = self.check(r=request(purposes=["public_retrieval", "model_training"]), expected="no_grant")
        self.assertEqual(len(result["tuples"]), 2)

    def test_action_purpose_cartesian_product(self):
        result = self.check(r=request(actions=["retrieve", "store"], purposes=["public_retrieval", "internal_knowledge"]), expected="no_grant")
        self.assertEqual(len(result["tuples"]), 4)

    def test_global_prohibit_dominates_narrow_permission(self):
        p = policy()
        p["resources"].append({"id": "broad", "selectors": [{"type": "directory_prefix", "path": "/", "query": "any"}]})
        denial = rule("broad-denial", "prohibit")
        denial["resource_ids"] = ["broad"]
        p["rules"].append(denial)
        self.check(p, expected="prohibit")

    def test_require_agreement_dominates_permit(self):
        p = policy()
        p["rules"].append(rule("explicit-assent", "require_agreement"))
        result = self.check(p, expected="require_agreement")
        self.assertEqual(result["tuples"][0]["rule_ids"], ["explicit-assent", "public-read"])

    def test_require_agreement_is_not_satisfied_by_support(self):
        p = policy()
        p["rules"][0]["effect"] = "require_agreement"
        self.check(p, request(support=sorted(odexa.DUTIES)), "require_agreement")

    def test_no_grant_dominates_agreement_for_combined_request(self):
        p = policy()
        p["rules"][0]["effect"] = "require_agreement"
        self.check(p, request(actions=["retrieve", "store"]), "no_grant")

    def test_unknown_duty_invalidates_policy(self):
        p = policy()
        p["rules"][0]["obligations"] = [{"type": "magic-compliance"}]
        self.check(p, expected="invalid_policy")

    def test_known_unsupported_duty_never_permits(self):
        p = policy()
        p["rules"][0]["obligations"] = [{"type": "retention", "max_seconds": 10}]
        self.check(p, expected="unsupported")

    def test_supported_duty_remains_unverified(self):
        p = policy()
        p["rules"][0]["obligations"] = [{"type": "retention", "max_seconds": 10}]
        result = self.check(p, request(support=["retention"]))
        self.assertEqual(result["obligation_status"], "not_verified")

    def test_prohibit_dominates_unsupported_duty(self):
        p = policy()
        p["rules"][0]["obligations"] = [{"type": "retention", "max_seconds": 10}]
        p["rules"].append(rule("denial", "prohibit"))
        self.check(p, expected="prohibit")

    def test_no_grant_dominates_unsupported_on_other_tuple(self):
        p = policy()
        p["rules"][0]["obligations"] = [{"type": "retention", "max_seconds": 10}]
        self.check(p, request(actions=["retrieve", "store"]), "no_grant")

    def test_duties_accumulate_deduplicate_and_tighten(self):
        p = policy()
        attr = {"type": "attribution", "name": "Origin", "url": "https://origin.example/about"}
        p["rules"][0]["obligations"] = [attr, {"type": "retention", "max_seconds": 100}, {"type": "report_usage", "endpoint": "https://origin.example/report", "deadline_seconds": 30}]
        p["rules"].append(rule("more-duties", duties=[dict(attr), {"type": "retention", "max_seconds": 0}, {"type": "report_usage", "endpoint": "https://origin.example/report", "deadline_seconds": 10}, {"type": "report_usage", "endpoint": "https://origin.example/other-report", "deadline_seconds": 20}]))
        result = self.check(p, request(support=sorted(odexa.DUTIES)))
        self.assertEqual(len(result["obligations"]), 4)
        self.assertIn({"type": "retention", "max_seconds": 0}, result["obligations"])
        self.assertIn({"type": "report_usage", "endpoint": "https://origin.example/report", "deadline_seconds": 10}, result["obligations"])

    def test_rule_order_does_not_change_output(self):
        p = policy()
        p["rules"].append(rule("second", "require_agreement"))
        first = self.check(p, expected="require_agreement")
        p["rules"].reverse()
        self.assertEqual(first, self.check(p, expected="require_agreement"))

    def test_resource_overlap_does_not_duplicate_rules(self):
        p = policy()
        p["resources"].append({"id": "other", "selectors": [{"type": "directory_prefix", "path": "/licensed/", "query": "any"}]})
        p["rules"][0]["resource_ids"].append("other")
        result = self.check(p)
        self.assertEqual(result["tuples"][0]["rule_ids"], ["public-read"])

    def test_prefix_matches_directory_itself(self):
        p = policy()
        p["resources"][0]["selectors"] = [{"type": "directory_prefix", "path": "/licensed/", "query": "absent"}]
        self.check(p, request("https://origin.example/licensed/"))

    def test_prefix_does_not_match_directory_without_slash(self):
        p = policy()
        p["resources"][0]["selectors"] = [{"type": "directory_prefix", "path": "/licensed/", "query": "absent"}]
        self.check(p, request("https://origin.example/licensed"), "no_grant")

    def test_prefix_does_not_match_lookalike(self):
        p = policy()
        p["resources"][0]["selectors"] = [{"type": "directory_prefix", "path": "/licensed/", "query": "absent"}]
        self.check(p, request("https://origin.example/licensed-old/about.txt"), "no_grant")

    def test_query_any_covers_empty_and_nonempty_query(self):
        p = policy()
        p["resources"][0]["selectors"][0]["query"] = "any"
        for suffix in ("?", "?q=%2Ffoo", "?q=x#fragment"):
            result = self.check(p, request("https://origin.example/licensed/about.txt" + suffix))
            self.assertTrue(result["query_present"])

    def test_utf8_path_is_preserved_without_unicode_normalization(self):
        p = policy()
        p["resources"][0]["selectors"][0]["path"] = "/caf%C3%A9"
        self.check(p, request("https://origin.example/caf%c3%a9"))
        self.check(p, request("https://origin.example/cafe%CC%81"), "no_grant")

    def test_default_empty_path_becomes_root(self):
        p = policy()
        p["resources"][0]["selectors"][0]["path"] = "/"
        self.check(p, request("https://origin.example"))

    def test_canonical_ipv4_supported(self):
        p = policy()
        p["origin"] = "https://127.0.0.1"
        p["policy_id"] = p["origin"] + "/policy"
        self.check(p, request("https://127.0.0.1/licensed/about.txt"))

    def test_decimal_integer_token_rejected(self):
        self.bad_file(b'{"revision":1.0}')

    def test_negative_zero_integer_token_rejected(self):
        self.bad_file(b'{"max_seconds":-0}')

    def test_exponent_integer_token_rejected(self):
        self.bad_file(b'{"revision":1e0}')

    def test_rounding_cannot_hide_fraction(self):
        self.bad_file(b'{"revision":1.0000000000000001}')

    def test_rounding_cannot_hide_large_fraction(self):
        self.bad_file(b'{"revision":9007199254740990.6}')

    def test_exact_issued_boundary_is_valid(self):
        self.check(now=BASE["issued_at"])

    def test_before_issuance_is_not_valid(self):
        self.check(expected="not_yet_valid", now="2026-09-15T23:59:59Z")

    def test_exact_expiry_is_expired(self):
        self.check(expected="expired", now=BASE["expires_at"])

    def test_policy_history_not_silently_compared(self):
        p = policy()
        p["revision"] += 1
        result = self.check(p)
        self.assertEqual(result["revision"], 2)

    def test_duplicate_json_keys_rejected(self):
        self.bad_file(b'{"revision":1,"revision":2}')

    def test_invalid_utf8_rejected(self):
        self.bad_file(b'{"name":"\xff"}')

    def test_non_json_number_rejected(self):
        self.bad_file(b'{"revision":NaN}')

    def test_oversized_policy_rejected(self):
        self.bad_file(b" " * (odexa.MAX_BYTES + 1))

    def test_deep_document_rejected(self):
        self.bad_file(("[" * 40 + "0" + "]" * 40).encode())

    def test_huge_integer_does_not_crash_loader(self):
        self.bad_file(("{\"revision\":" + "9" * 5000 + "}").encode())

    def bad_file(self, raw):
        with tempfile.TemporaryDirectory() as folder:
            path = Path(folder) / "policy.json"
            path.write_bytes(raw)
            with self.assertRaises(odexa.ValidationError):
                odexa.load_policy(path)

    def test_cli_validate_and_evaluate(self):
        cli = ROOT / "odexa_ref" / "policy.py"
        example = ROOT / "examples" / "free-basic.json"
        validation = subprocess.run([sys.executable, str(cli), "validate", str(example)], text=True, capture_output=True)
        self.assertEqual(validation.returncode, 0, validation.stderr)
        self.assertTrue(json.loads(validation.stdout)["valid"])
        evaluation = subprocess.run([sys.executable, str(cli), "evaluate", str(example), "--url", "https://origin.example/licensed/about.txt", "--action", "retrieve", "--purpose", "public_retrieval", "--now", NOW], text=True, capture_output=True)
        self.assertEqual(evaluation.returncode, 0, evaluation.stderr)
        self.assertEqual(json.loads(evaluation.stdout)["decision"], "permit")

    def test_cli_invalid_request_exits_two(self):
        result = subprocess.run([sys.executable, str(ROOT / "odexa_ref" / "policy.py"), "evaluate", str(ROOT / "examples" / "free-basic.json"), "--url", "http://origin.example/", "--action", "retrieve", "--purpose", "public_retrieval"], text=True, capture_output=True)
        self.assertEqual(result.returncode, 2)
        self.assertEqual(json.loads(result.stdout)["decision"], "invalid_request")


URL_CASES = [
    ("uppercase_host_default_port", "HTTPS://ORIGIN.EXAMPLE:443/licensed/about.txt", "permit"),
    ("unreserved_encoding", "https://origin.example/licensed/%61bout.txt", "permit"),
    ("fragment_ignored", "https://origin.example/licensed/about.txt#section", "permit"),
    ("fragment_question_not_query", "https://origin.example/licensed/about.txt#?x", "permit"),
    ("bare_query_not_absent", "https://origin.example/licensed/about.txt?", "no_grant"),
    ("query_excluded", "https://origin.example/licensed/about.txt?q=x", "no_grant"),
    ("path_case_sensitive", "https://origin.example/licensed/About.txt", "no_grant"),
    ("other_origin", "https://other.example/licensed/about.txt", "no_grant"),
    ("other_subdomain", "https://www.origin.example/licensed/about.txt", "no_grant"),
    ("other_port", "https://origin.example:444/licensed/about.txt", "no_grant"),
    ("http_rejected", "http://origin.example/licensed/about.txt", "invalid_request"),
    ("userinfo_rejected", "https://person@origin.example/licensed/about.txt", "invalid_request"),
    ("dot_segment", "https://origin.example/licensed/../licensed/about.txt", "invalid_request"),
    ("encoded_dot_segment", "https://origin.example/licensed/%2e%2e/licensed/about.txt", "invalid_request"),
    ("single_dot_segment", "https://origin.example/licensed/./about.txt", "invalid_request"),
    ("encoded_slash", "https://origin.example/licensed%2Fabout.txt", "invalid_request"),
    ("encoded_backslash", "https://origin.example/licensed%5cabout.txt", "invalid_request"),
    ("raw_backslash", "https://origin.example/licensed\\about.txt", "invalid_request"),
    ("duplicate_slash", "https://origin.example/licensed//about.txt", "invalid_request"),
    ("malformed_escape", "https://origin.example/licensed/%G0about.txt", "invalid_request"),
    ("incomplete_escape", "https://origin.example/licensed/%", "invalid_request"),
    ("double_encoding", "https://origin.example/licensed/%252E%252E/about.txt", "invalid_request"),
    ("encoded_nul", "https://origin.example/licensed/%00about.txt", "invalid_request"),
    ("encoded_space", "https://origin.example/licensed/%20about.txt", "invalid_request"),
    ("raw_space", "https://origin.example/licensed/about .txt", "invalid_request"),
    ("raw_control", "https://origin.example/licensed/\nabout.txt", "invalid_request"),
    ("raw_unicode", "https://origin.example/caf\u00e9", "invalid_request"),
    ("truncated_utf8", "https://origin.example/%C3", "invalid_request"),
    ("overlong_utf8", "https://origin.example/%C0%AF", "invalid_request"),
    ("encoded_surrogate", "https://origin.example/%ED%A0%80", "invalid_request"),
    ("ipv6_out_of_profile", "https://[::1]/licensed/about.txt", "invalid_request"),
    ("numeric_alias", "https://127.1/licensed/about.txt", "invalid_request"),
    ("ipv4_leading_zero", "https://127.0.0.01/licensed/about.txt", "invalid_request"),
    ("trailing_host_dot", "https://origin.example./licensed/about.txt", "invalid_request"),
    ("zero_port", "https://origin.example:0/licensed/about.txt", "invalid_request"),
    ("large_port", "https://origin.example:65536/licensed/about.txt", "invalid_request"),
    ("empty_port", "https://origin.example:/licensed/about.txt", "invalid_request"),
    ("leading_zero_port", "https://origin.example:0443/licensed/about.txt", "invalid_request"),
]


def url_test(url, expected):
    def run(self):
        self.check(r=request(url), expected=expected)
    return run


for name, url, expected in URL_CASES:
    setattr(EvaluatorTests, "test_url_" + name, url_test(url, expected))


POLICY_MUTATIONS = [
    ("zero_revision", lambda p: p.update(revision=0)),
    ("boolean_revision", lambda p: p.update(revision=True)),
    ("float_revision", lambda p: p.update(revision=1.0)),
    ("wrong_version", lambda p: p.update(protocol_version="1.2.0")),
    ("unknown_root_field", lambda p: p.update(provider="mandatory")),
    ("noncanonical_origin", lambda p: p.update(origin="https://ORIGIN.example")),
    ("origin_trailing_slash", lambda p: p.update(origin="https://origin.example/")),
    ("foreign_policy_id", lambda p: p.update(policy_id="https://other.example/policy")),
    ("query_policy_id", lambda p: p.update(policy_id="https://origin.example/policy?")),
    ("bad_timestamp", lambda p: p.update(issued_at="2026-02-30T00:00:00Z")),
    ("fractional_timestamp", lambda p: p.update(issued_at="2026-09-16T00:00:00.0Z")),
    ("inverted_times", lambda p: p.update(expires_at="2026-09-15T00:00:00Z")),
    ("permissive_default", lambda p: p.update(default="permit")),
    ("unknown_resource", lambda p: p["rules"][0].update(resource_ids=["missing"])),
    ("duplicate_rule_id", lambda p: p["rules"].append(copy.deepcopy(p["rules"][0]))),
    ("duplicate_resource_id", lambda p: p["resources"].append(copy.deepcopy(p["resources"][0]))),
    ("unknown_action", lambda p: p["rules"][0].update(actions=["train"])),
    ("unknown_purpose", lambda p: p["rules"][0].update(purposes=["ai"])),
    ("duplicate_action", lambda p: p["rules"][0].update(actions=["retrieve", "retrieve"])),
    ("bad_prefix", lambda p: p["resources"][0]["selectors"][0].update(type="directory_prefix", path="/licensed")),
    ("noncanonical_selector", lambda p: p["resources"][0]["selectors"][0].update(path="/licensed/%61bout.txt")),
    ("selector_dot_segment", lambda p: p["resources"][0]["selectors"][0].update(path="/licensed/../about.txt")),
    ("selector_type_object", lambda p: p["resources"][0]["selectors"][0].update(type={})),
    ("negative_retention", lambda p: p["rules"][0].update(obligations=[{"type": "retention", "max_seconds": -1}])),
    ("zero_report_deadline", lambda p: p["rules"][0].update(obligations=[{"type": "report_usage", "endpoint": "https://origin.example/report", "deadline_seconds": 0}])),
    ("secret_query_report_endpoint", lambda p: p["rules"][0].update(obligations=[{"type": "report_usage", "endpoint": "https://origin.example/report?token=secret", "deadline_seconds": 1}])),
    ("duty_on_prohibition", lambda p: p["rules"][0].update(effect="prohibit", obligations=[{"type": "retention", "max_seconds": 5}])),
]


def mutation_test(mutate):
    def run(self):
        p = policy()
        mutate(p)
        self.check(p, expected="invalid_policy")
    return run


for name, mutation in POLICY_MUTATIONS:
    setattr(EvaluatorTests, "test_invalid_policy_" + name, mutation_test(mutation))


REQUEST_MUTATIONS = [
    ("empty_actions", lambda r: r.update(actions=[])),
    ("empty_purposes", lambda r: r.update(purposes=[])),
    ("unknown_action", lambda r: r.update(actions=["read"])),
    ("unknown_purpose", lambda r: r.update(purposes=["anything"])),
    ("duplicate_purpose", lambda r: r.update(purposes=["public_retrieval", "public_retrieval"])),
    ("unknown_capability", lambda r: r.update(supported_obligations=["all"])),
    ("extra_field", lambda r: r.update(assume_permission=True)),
]


def request_mutation_test(mutate):
    def run(self):
        r = request()
        mutate(r)
        self.check(r=r, expected="invalid_request")
    return run


for name, mutation in REQUEST_MUTATIONS:
    setattr(EvaluatorTests, "test_invalid_request_" + name, request_mutation_test(mutation))


if __name__ == "__main__":
    unittest.main()
