import unittest
from unittest.mock import Mock, patch
from urllib.parse import parse_qs, urlparse

from ezpanos.ezpanos import EzPanOS


class TestSecurityRuleManagement(unittest.TestCase):
    def _client(self) -> EzPanOS:
        return EzPanOS(endpoint="10.0.0.1", api_key="test-key", connect_on_init=False)

    def test_execute_supports_cmd_element_and_extra_params(self):
        client = self._client()
        mocked_response = Mock()
        mocked_response.raise_for_status.return_value = None
        mocked_response.text = '<response status="success" code="20"><result/></response>'

        with patch("ezpanos.ezpanos.requests.post", return_value=mocked_response) as mocked_post:
            response = client.execute(
                api_type="config",
                api_action="set",
                api_xpath="/config/devices/entry",
                api_cmd="<set><foo>bar</foo></set>",
                api_element="<entry name='x'></entry>",
                api_params={"where": "before", "dst": "anchor-rule"},
            )

        self.assertIsInstance(response, dict)
        self.assertTrue(mocked_post.called)
        url = mocked_post.call_args.args[0]
        parsed = parse_qs(urlparse(url).query)
        self.assertEqual(parsed.get("type", [None])[0], "config")
        self.assertEqual(parsed.get("action", [None])[0], "set")
        self.assertEqual(parsed.get("xpath", [None])[0], "/config/devices/entry")
        self.assertEqual(parsed.get("where", [None])[0], "before")
        self.assertEqual(parsed.get("dst", [None])[0], "anchor-rule")
        self.assertEqual(parsed.get("cmd", [None])[0], "<set><foo>bar</foo></set>")
        self.assertEqual(parsed.get("element", [None])[0], "<entry name='x'></entry>")

    def test_create_security_rule_with_move(self):
        client = self._client()
        with patch.object(client, "_default_vsys_name", return_value="vsys1"):
            with patch.object(
                client,
                "execute",
                side_effect=[
                    {"response": {"status": "success", "code": "20", "result": {}}},
                    {"response": {"status": "success", "code": "20", "result": {}}},
                ],
            ) as mocked_execute:
                result = client.create_security_rule(
                    rule_name="test-rule",
                    from_zones=["trust"],
                    to_zones=["untrust"],
                    sources=["any"],
                    destinations=["any"],
                    applications=["web-browsing"],
                    services=["application-default"],
                    action="allow",
                    position="before",
                    reference_rule="existing-rule",
                )

        self.assertTrue(result["created"])
        self.assertTrue(result["moved"])
        self.assertEqual(mocked_execute.call_count, 2)
        first_call = mocked_execute.call_args_list[0].kwargs
        second_call = mocked_execute.call_args_list[1].kwargs
        self.assertEqual(first_call["api_type"], "config")
        self.assertEqual(first_call["api_action"], "set")
        self.assertIn("<entry name=\"test-rule\">", first_call["api_element"])
        self.assertEqual(second_call["api_action"], "move")
        self.assertEqual(second_call["api_params"]["where"], "before")
        self.assertEqual(second_call["api_params"]["dst"], "existing-rule")

    def test_delete_security_rule_success(self):
        client = self._client()
        with patch.object(client, "_default_vsys_name", return_value="vsys1"):
            with patch.object(
                client,
                "execute",
                side_effect=[
                    {"response": {"status": "success", "result": {"entry": {"name": "deleteme"}}}},
                    {"response": {"status": "success", "code": "20", "result": {}}},
                    {"response": {"status": "error", "code": "7", "msg": {"line": "not found"}}},
                ],
            ):
                result = client.delete_security_rule("deleteme")

        self.assertTrue(result["exists_before"])
        self.assertFalse(result["exists_after"])
        self.assertTrue(result["deleted"])
        self.assertEqual(result["delete_status"], "success")

    def test_delete_security_rule_missing_ignore(self):
        client = self._client()
        with patch.object(client, "_default_vsys_name", return_value="vsys1"):
            with patch.object(
                client,
                "execute",
                return_value={"response": {"status": "error", "code": "7", "msg": {"line": "not found"}}},
            ):
                result = client.delete_security_rule("missing-rule", ignore_missing=True)

        self.assertFalse(result["deleted"])
        self.assertEqual(result["reason"], "rule_not_found")

    def test_commit_waits_for_job_until_fin(self):
        client = self._client()
        with patch.object(
            client,
            "execute",
            side_effect=[
                {"response": {"status": "success", "code": "19", "result": {"job": "77"}}},
                {"response": {"status": "success", "result": {"job": {"id": "77", "status": "ACT", "result": "PEND"}}}},
                {"response": {"status": "success", "result": {"job": {"id": "77", "status": "FIN", "result": "OK"}}}},
            ],
        ) as mocked_execute:
            result = client.commit(wait_for_job=True, timeout=5, poll_interval=0.01)

        self.assertEqual(mocked_execute.call_count, 3)
        self.assertEqual(result["job_id"], "77")
        self.assertTrue(result["job_complete"])
        self.assertEqual(result["job_status"], "FIN")
        self.assertEqual(result["job_result"], "OK")
        self.assertTrue(result["success"])

    def test_policy_defaults_merge_profile_and_endpoint(self):
        profile_block = {
            "policy_defaults": {
                "rule_create": {"services": ["application-default"], "log_end": True},
                "commit": {"enabled": False},
            },
            "endpoints": [
                {
                    "endpoint": "10.0.6.2",
                    "policy_defaults": {
                        "rule_create": {"services": ["svc-custom"], "log_end": False},
                        "commit": {"enabled": True},
                    },
                }
            ],
        }

        merged = EzPanOS._resolve_policy_defaults_for_endpoint(profile_block, "10.0.6.2")
        self.assertEqual(merged["rule_create"]["services"], ["svc-custom"])
        self.assertFalse(merged["rule_create"]["log_end"])
        self.assertTrue(merged["commit"]["enabled"])

    def test_create_security_rule_uses_policy_defaults_and_auto_commit(self):
        client = self._client()
        client.policy_defaults = {
            "rule_create": {
                "from_zones": ["trust"],
                "to_zones": ["untrust"],
                "applications": ["web-browsing"],
                "services": ["application-default"],
                "log_end": True,
                "description_prefix": "[auto]",
            },
            "commit": {
                "enabled": True,
                "wait_for_job": True,
                "timeout": 10,
                "poll_interval": 0.01,
            },
        }

        with patch.object(client, "_default_vsys_name", return_value="vsys1"):
            with patch.object(
                client,
                "execute",
                return_value={"response": {"status": "success", "code": "20", "result": {}}},
            ) as mocked_execute:
                with patch.object(client, "commit", return_value={"success": True, "job_id": "1"}) as mocked_commit:
                    result = client.create_security_rule(
                        rule_name="defaults-rule",
                        sources=["any"],
                        destinations=["any"],
                    )

        self.assertTrue(result["created"])
        self.assertTrue(result["committed"])
        self.assertEqual(result["request"]["from"], ["trust"])
        self.assertEqual(result["request"]["to"], ["untrust"])
        self.assertEqual(result["request"]["application"], ["web-browsing"])
        self.assertEqual(result["request"]["service"], ["application-default"])
        self.assertEqual(result["request"]["action"], "allow")
        self.assertIn("[auto]", mocked_execute.call_args.kwargs["api_element"])
        mocked_commit.assert_called_once()


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