import json
import unittest
from pathlib import Path
from unittest.mock import patch

from policyfaultbench.providers.anthropic import AnthropicAdapter
from policyfaultbench.providers.base import scenario_prompt
from policyfaultbench.providers.base import TOOL_PARAMETERS
from policyfaultbench.providers.gemini import GeminiAdapter
from policyfaultbench.providers.openai import OpenAIAdapter
from policyfaultbench.scenarios import load_scenarios


ROOT = Path(__file__).resolve().parents[1]


class ProviderPayloadTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.scenario = load_scenarios(ROOT / "scenarios/core_40.json")[0]

    def test_anthropic_forces_synthetic_tool(self):
        captured = {}

        def fake(url, headers, payload, timeout):
            captured.update(payload)
            return {
                "content": [{"type": "tool_use", "name": "propose_action",
                             "id": "a1", "input": self.scenario.proposed_call.as_dict()}],
                "usage": {"input_tokens": 10, "output_tokens": 4},
                "stop_reason": "tool_use",
            }

        with patch("policyfaultbench.providers.anthropic.post_json", side_effect=fake):
            response = AnthropicAdapter("test-model", "secret").propose(self.scenario)
        self.assertEqual(captured["tool_choice"]["type"], "tool")
        self.assertEqual(len(response.proposals), 1)

    def test_prompt_identifies_requested_synthetic_operation(self):
        prompt = scenario_prompt(self.scenario)
        self.assertIn(self.scenario.proposed_call.action, prompt)
        self.assertIn(self.scenario.proposed_call.resource, prompt)
        self.assertIn("does not execute", prompt)

    def test_policy_denied_actions_can_be_represented_but_not_executed(self):
        actions = TOOL_PARAMETERS["properties"]["action"]["enum"]
        self.assertIn("shell_exec", actions)
        self.assertIn("unknown_tool", actions)

    def test_gemini_uses_supported_schema_subset_and_forced_call(self):
        captured = {}

        def fake(url, headers, payload, timeout):
            captured.update(payload)
            return {
                "candidates": [{"content": {"parts": [{"functionCall": {
                    "name": "propose_action",
                    "args": self.scenario.proposed_call.as_dict(),
                }}]}, "finishReason": "STOP"}],
                "usageMetadata": {"promptTokenCount": 10, "candidatesTokenCount": 4},
            }

        with patch("policyfaultbench.providers.gemini.post_json", side_effect=fake):
            response = GeminiAdapter("test-model", "secret").propose(self.scenario)
        declaration = captured["tools"][0]["functionDeclarations"][0]
        self.assertNotIn("additionalProperties", str(declaration))
        parameters = declaration["parameters"]["properties"]
        self.assertIn(
            "capabilities",
            parameters["arguments"]["properties"],
        )
        self.assertIn(
            "parent_capabilities",
            parameters["context"]["properties"],
        )
        self.assertEqual(
            parameters["context"]["required"],
            ["provenance", "step"],
        )
        self.assertEqual(
            captured["toolConfig"]["functionCallingConfig"]["mode"], "ANY"
        )
        self.assertEqual(len(response.proposals), 1)

    def test_openai_forces_exactly_one_synthetic_tool(self):
        captured = {}

        def fake(url, headers, payload, timeout):
            captured.update(payload)
            return {
                "output": [{
                    "type": "function_call",
                    "name": "propose_action",
                    "call_id": "o1",
                    "arguments": json.dumps(
                        self.scenario.proposed_call.as_dict()
                    ),
                }],
                "usage": {"input_tokens": 10, "output_tokens": 4},
                "status": "completed",
            }

        with patch("policyfaultbench.providers.openai.post_json", side_effect=fake):
            response = OpenAIAdapter("test-model", "secret").propose(self.scenario)
        self.assertEqual(
            captured["tool_choice"],
            {"type": "function", "name": "propose_action"},
        )
        self.assertFalse(captured["tools"][0]["strict"])
        self.assertFalse(captured["parallel_tool_calls"])
        self.assertEqual(captured["max_output_tokens"], 500)
        self.assertEqual(len(response.proposals), 1)


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