"""Tests for kv provider registry — auth injection and URL building."""

import unittest

from kv.providers import (
    get_provider, list_providers, build_auth, build_url, PROVIDERS,
)


class TestProviderRegistry(unittest.TestCase):

    def test_all_providers_exist(self):
        for name in ["openai", "anthropic", "google", "github", "google-cloud"]:
            self.assertIsNotNone(get_provider(name), f"missing provider: {name}")

    def test_unknown_provider_returns_none(self):
        self.assertIsNone(get_provider("nonexistent"))

    def test_list_providers(self):
        names = list_providers()
        self.assertIn("openai", names)
        self.assertIn("anthropic", names)
        self.assertIsInstance(names, list)

    def test_all_providers_have_required_fields(self):
        for name, config in PROVIDERS.items():
            self.assertIn("base_url", config, f"{name} missing base_url")
            self.assertIn("secret_name", config, f"{name} missing secret_name")
            self.assertIn("auth_type", config, f"{name} missing auth_type")


class TestBuildAuth(unittest.TestCase):

    def test_bearer_auth(self):
        config = get_provider("openai")
        headers, params = build_auth(config, "sk-test-key")
        self.assertEqual(headers["Authorization"], "Bearer sk-test-key")
        self.assertEqual(params, {})

    def test_x_api_key_auth(self):
        config = get_provider("anthropic")
        headers, params = build_auth(config, "sk-ant-test")
        self.assertEqual(headers["x-api-key"], "sk-ant-test")
        self.assertNotIn("Authorization", headers)
        self.assertEqual(params, {})

    def test_query_param_auth(self):
        config = get_provider("google")
        headers, params = build_auth(config, "AIza-test")
        self.assertNotIn("Authorization", headers)
        self.assertEqual(params, {"key": "AIza-test"})

    def test_default_headers_included(self):
        config = get_provider("openai")
        headers, _ = build_auth(config, "test")
        self.assertEqual(headers["Content-Type"], "application/json")

    def test_anthropic_version_header(self):
        config = get_provider("anthropic")
        headers, _ = build_auth(config, "test")
        self.assertEqual(headers["anthropic-version"], "2023-06-01")

    def test_github_accept_header(self):
        config = get_provider("github")
        headers, _ = build_auth(config, "test")
        self.assertEqual(headers["Accept"], "application/vnd.github+json")

    def test_secret_not_in_wrong_place(self):
        """Bearer auth should NOT put key in query params."""
        config = get_provider("openai")
        _, params = build_auth(config, "sk-secret")
        self.assertNotIn("sk-secret", str(params))

    def test_query_auth_not_in_headers(self):
        """Query auth should NOT put key in Authorization header."""
        config = get_provider("google")
        headers, _ = build_auth(config, "AIza-secret")
        self.assertNotIn("Authorization", headers)
        self.assertNotIn("AIza-secret", str(headers))


class TestBuildUrl(unittest.TestCase):

    def test_simple_path(self):
        config = get_provider("openai")
        url = build_url(config, "/v1/chat/completions")
        self.assertEqual(url, "https://api.openai.com/v1/chat/completions")

    def test_path_without_leading_slash(self):
        config = get_provider("openai")
        url = build_url(config, "v1/models")
        self.assertEqual(url, "https://api.openai.com/v1/models")

    def test_query_params_appended(self):
        config = get_provider("google")
        url = build_url(config, "/v1/models", {"key": "test123"})
        self.assertIn("key=test123", url)
        self.assertIn("?", url)

    def test_base_url_trailing_slash_handled(self):
        config = {"base_url": "https://api.example.com/", "auth_type": "bearer"}
        url = build_url(config, "/v1/test")
        self.assertEqual(url, "https://api.example.com/v1/test")


class TestHeaderAuth(unittest.TestCase):
    """Tests for the 'header' auth type (custom header name)."""

    def test_header_auth_custom_name(self):
        config = {
            "auth_type": "header",
            "auth_header_name": "X-Api-Token",
            "default_headers": {"Content-Type": "application/json"},
        }
        headers, params = build_auth(config, "my-secret-key")
        self.assertEqual(headers["X-Api-Token"], "my-secret-key")
        self.assertEqual(params, {})

    def test_header_auth_default_name(self):
        config = {
            "auth_type": "header",
            "default_headers": {},
        }
        headers, _ = build_auth(config, "token123")
        self.assertEqual(headers["Authorization"], "token123")

    def test_basic_auth_format(self):
        config = {
            "auth_type": "basic",
            "default_headers": {},
        }
        headers, _ = build_auth(config, "my-password")
        self.assertTrue(headers["Authorization"].startswith("Basic "))
        import base64
        decoded = base64.b64decode(headers["Authorization"][6:]).decode()
        self.assertEqual(decoded, ":my-password")


class TestCustomProviders(unittest.TestCase):
    """Tests for dynamic provider loading from config."""

    def setUp(self):
        import tempfile, json, os
        self._tmpdir = tempfile.mkdtemp()
        self._sdir = os.path.join(self._tmpdir, ".secrets")
        os.makedirs(self._sdir)
        # Save original PROVIDERS state
        self._orig_providers = dict(PROVIDERS)

    def tearDown(self):
        # Restore original PROVIDERS
        PROVIDERS.clear()
        PROVIDERS.update(self._orig_providers)

    def _write_config(self, config):
        import json, os
        with open(os.path.join(self._sdir, "config.json"), "w") as f:
            json.dump(config, f)

    def test_load_custom_provider(self):
        from kv.providers import load_custom_providers
        self._write_config({
            "version": 1, "default_env": "dev", "environments": ["dev"],
            "providers": {
                "stripe": {
                    "base_url": "https://api.stripe.com",
                    "secret_name": "STRIPE_KEY",
                    "auth_type": "bearer",
                }
            }
        })
        count = load_custom_providers(self._tmpdir)
        self.assertEqual(count, 1)
        self.assertIsNotNone(get_provider("stripe"))
        self.assertEqual(get_provider("stripe")["base_url"], "https://api.stripe.com")

    def test_custom_merges_with_builtins(self):
        from kv.providers import load_custom_providers
        self._write_config({
            "version": 1, "default_env": "dev", "environments": ["dev"],
            "providers": {
                "twilio": {
                    "base_url": "https://api.twilio.com",
                    "secret_name": "TWILIO_KEY",
                    "auth_type": "basic",
                }
            }
        })
        load_custom_providers(self._tmpdir)
        # Built-in still works
        self.assertIsNotNone(get_provider("openai"))
        # Custom added
        self.assertIsNotNone(get_provider("twilio"))
        self.assertIn("twilio", list_providers())

    def test_invalid_provider_skipped(self):
        from kv.providers import load_custom_providers
        self._write_config({
            "version": 1, "default_env": "dev", "environments": ["dev"],
            "providers": {
                "bad1": {"base_url": "https://x.com"},  # missing secret_name
                "bad2": {"secret_name": "KEY"},           # missing base_url
                "bad3": "not a dict",
                "good": {
                    "base_url": "https://api.good.com",
                    "secret_name": "GOOD_KEY",
                    "auth_type": "bearer",
                }
            }
        })
        count = load_custom_providers(self._tmpdir)
        self.assertEqual(count, 1)  # only "good"
        self.assertIsNone(get_provider("bad1"))
        self.assertIsNotNone(get_provider("good"))

    def test_invalid_auth_type_skipped(self):
        from kv.providers import load_custom_providers
        self._write_config({
            "version": 1, "default_env": "dev", "environments": ["dev"],
            "providers": {
                "evil": {
                    "base_url": "https://evil.com",
                    "secret_name": "KEY",
                    "auth_type": "magic",  # not valid
                }
            }
        })
        count = load_custom_providers(self._tmpdir)
        self.assertEqual(count, 0)

    def test_no_providers_key(self):
        from kv.providers import load_custom_providers
        self._write_config({
            "version": 1, "default_env": "dev", "environments": ["dev"],
        })
        count = load_custom_providers(self._tmpdir)
        self.assertEqual(count, 0)

    def test_custom_overrides_builtin(self):
        from kv.providers import load_custom_providers
        self._write_config({
            "version": 1, "default_env": "dev", "environments": ["dev"],
            "providers": {
                "openai": {
                    "base_url": "https://custom-openai-proxy.com",
                    "secret_name": "CUSTOM_KEY",
                    "auth_type": "bearer",
                }
            }
        })
        load_custom_providers(self._tmpdir)
        p = get_provider("openai")
        self.assertEqual(p["base_url"], "https://custom-openai-proxy.com")
        self.assertEqual(p["secret_name"], "CUSTOM_KEY")


class TestSSRFProtection(unittest.TestCase):
    """Tests for URL safety validation — SSRF prevention."""

    def test_https_allowed(self):
        from kv.providers import _check_url_safety
        safe, _ = _check_url_safety("https://api.openai.com/v1/models")
        self.assertTrue(safe)

    def test_http_blocked(self):
        from kv.providers import _check_url_safety
        safe, reason = _check_url_safety("http://api.example.com/v1")
        self.assertFalse(safe)
        self.assertIn("non-HTTPS", reason)

    def test_localhost_blocked(self):
        from kv.providers import _check_url_safety
        safe, _ = _check_url_safety("https://localhost/v1")
        self.assertFalse(safe)

    def test_zero_ip_blocked(self):
        from kv.providers import _check_url_safety
        safe, _ = _check_url_safety("https://0.0.0.0/v1")
        self.assertFalse(safe)

    def test_loopback_blocked(self):
        from kv.providers import _check_url_safety
        safe, reason = _check_url_safety("https://127.0.0.1/v1")
        self.assertFalse(safe)
        self.assertIn("private", reason.lower())

    def test_private_10_blocked(self):
        from kv.providers import _check_url_safety
        safe, _ = _check_url_safety("https://10.0.0.1/v1")
        self.assertFalse(safe)

    def test_private_172_blocked(self):
        from kv.providers import _check_url_safety
        safe, _ = _check_url_safety("https://172.16.0.1/v1")
        self.assertFalse(safe)

    def test_private_192_blocked(self):
        from kv.providers import _check_url_safety
        safe, _ = _check_url_safety("https://192.168.1.1/v1")
        self.assertFalse(safe)

    def test_empty_hostname(self):
        from kv.providers import _check_url_safety
        safe, _ = _check_url_safety("https:///v1")
        self.assertFalse(safe)

    def test_public_apis_allowed(self):
        from kv.providers import _check_url_safety
        for url in [
            "https://api.stripe.com/v1/charges",
            "https://api.anthropic.com/v1/messages",
            "https://api.github.com/user",
        ]:
            safe, reason = _check_url_safety(url)
            self.assertTrue(safe, f"{url} blocked: {reason}")

    def test_builtin_providers_skip_check(self):
        """Built-in providers bypass SSRF check (trusted URLs)."""
        from kv.providers import _BUILTIN_URLS
        # All built-in base URLs should be in the trusted set
        for name, config in PROVIDERS.items():
            self.assertIn(config["base_url"], _BUILTIN_URLS,
                         f"built-in {name} not in trusted set")


class TestNetworkCommands(unittest.TestCase):
    """Tests for config-based network access policy."""

    def setUp(self):
        import tempfile, json, os
        self._tmpdir = tempfile.mkdtemp()
        self._sdir = os.path.join(self._tmpdir, ".secrets")
        os.makedirs(self._sdir)

    def _write_config(self, config):
        import json, os
        with open(os.path.join(self._sdir, "config.json"), "w") as f:
            json.dump(config, f)

    def test_no_network_commands_default(self):
        from kv.config import is_network_allowed
        self._write_config({
            "version": 1, "default_env": "dev", "environments": ["dev"],
        })
        result = is_network_allowed(self._tmpdir, ["python3", "app.py"])
        self.assertFalse(result)

    def test_network_allowed_for_listed_command(self):
        from kv.config import is_network_allowed
        self._write_config({
            "version": 1, "default_env": "dev", "environments": ["dev"],
            "security": {"network_commands": ["python3 migrate.py"]}
        })
        self.assertTrue(is_network_allowed(self._tmpdir,
                        ["python3", "migrate.py"]))

    def test_network_denied_for_unlisted_command(self):
        from kv.config import is_network_allowed
        self._write_config({
            "version": 1, "default_env": "dev", "environments": ["dev"],
            "security": {"network_commands": ["python3 migrate.py"]}
        })
        self.assertFalse(is_network_allowed(self._tmpdir,
                         ["python3", "app.py"]))

    def test_add_network_command(self):
        from kv.config import add_network_command, is_network_allowed
        self._write_config({
            "version": 1, "default_env": "dev", "environments": ["dev"],
            "security": {}
        })
        added = add_network_command(self._tmpdir, "node server.js")
        self.assertTrue(added)
        self.assertTrue(is_network_allowed(self._tmpdir,
                        ["node", "server.js"]))

    def test_add_duplicate_returns_false(self):
        from kv.config import add_network_command
        self._write_config({
            "version": 1, "default_env": "dev", "environments": ["dev"],
            "security": {"network_commands": ["npm start"]}
        })
        added = add_network_command(self._tmpdir, "npm start")
        self.assertFalse(added)

    def test_prefix_matching(self):
        from kv.config import is_network_allowed
        self._write_config({
            "version": 1, "default_env": "dev", "environments": ["dev"],
            "security": {"network_commands": ["python3 app.py"]}
        })
        # Extra args should still match (prefix)
        self.assertTrue(is_network_allowed(self._tmpdir,
                        ["python3", "app.py", "--port", "8080"]))


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

