import sys as _sys
from pathlib import Path as _Path
_sys.path.insert(0, str(_Path(__file__).resolve().parent))
from mutation_profile_fixture import shared_mutation_env  # noqa: E402
"""Tests for canonical JSON runtime-profile configuration (gitea_config) and
its integration into gitea_auth.get_profile / get_auth_header.

Covers: legacy env-only, JSON profile config, profile selection, multiple
profiles, missing profile, invalid JSON, env-override precedence, keychain and
env auth-reference parsing/resolution, token/password redaction in errors, and
no network calls during config load.
"""
import os
import sys
import json
import tempfile
import unittest
from unittest.mock import patch

sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent))

import gitea_config  # noqa: E402
import gitea_auth  # noqa: E402

CONFIG = {
    "version": 1,
    "profiles": {
        "prgs": {
            "base_url": "https://gitea.prgs.cc",
            "username": "jcwalker3",
            "auth": {"type": "keychain", "id": "prgs-gitea-token"},
            "default_owner": "Scaled-Tech-Consulting",
            "execution_profile": "personal-prgs",
        },
        "mdcps-env": {
            "base_url": "https://gitea.dadeschools.net",
            "username": "913443",
            "auth": {"type": "env", "name": "GITEA_TOKEN_MDCPS"},
            "default_owner": "Contractor",
            "execution_profile": "mdcps",
        },
    },
}


class _ConfigBase(unittest.TestCase):
    def setUp(self):
        self._dir = tempfile.TemporaryDirectory()
        self.path = os.path.join(self._dir.name, "profiles.json")
        self._write(CONFIG)

    def tearDown(self):
        self._dir.cleanup()

    def _write(self, obj):
        with open(self.path, "w", encoding="utf-8") as fh:
            fh.write(obj if isinstance(obj, str) else json.dumps(obj))

    def _env(self, profile="prgs", **extra):
        env = {"GITEA_MCP_CONFIG": self.path, "GITEA_MCP_PROFILE": profile}
        env.update(extra)
        return env


# ---------------------------------------------------------------------------
# Loading / selection
# ---------------------------------------------------------------------------
class TestLoadSelect(_ConfigBase):

    def test_legacy_env_only_returns_none(self):
        with patch.dict(os.environ, {}, clear=True):
            self.assertIsNone(gitea_config.load_config())
            self.assertIsNone(gitea_config.resolve_profile())

    def test_selected_profile_loads(self):
        with patch.dict(os.environ, self._env("prgs"), clear=True):
            p = gitea_config.resolve_profile()
        self.assertEqual(p["base_url"], "https://gitea.prgs.cc")
        self.assertEqual(p["execution_profile"], "personal-prgs")

    def test_multiple_profiles_select_correctly(self):
        with patch.dict(os.environ, self._env("mdcps-env"), clear=True):
            p = gitea_config.resolve_profile()
        self.assertEqual(p["username"], "913443")
        self.assertEqual(p["auth"]["type"], "env")

    def test_missing_file_unset_is_noop(self):
        with patch.dict(os.environ, {}, clear=True):
            self.assertIsNone(gitea_config.resolve_profile())

    def test_missing_file_set_raises(self):
        env = {"GITEA_MCP_CONFIG": self.path + ".nope", "GITEA_MCP_PROFILE": "prgs"}
        with patch.dict(os.environ, env, clear=True):
            with self.assertRaises(gitea_config.ConfigError) as ctx:
                gitea_config.resolve_profile()
        self.assertIn("missing file", str(ctx.exception))

    def test_invalid_json_raises_without_leaking_content(self):
        self._write('{"version":1,"profiles":{"prgs":{"auth":{"type":"env" BROKEN')
        with patch.dict(os.environ, self._env("prgs"), clear=True):
            with self.assertRaises(gitea_config.ConfigError) as ctx:
                gitea_config.resolve_profile()
        msg = str(ctx.exception)
        self.assertIn("invalid JSON", msg)
        self.assertNotIn("BROKEN", msg)  # no file content leaked

    def test_missing_profile_raises_with_available(self):
        with patch.dict(os.environ, self._env("ghost"), clear=True):
            with self.assertRaises(gitea_config.ConfigError) as ctx:
                gitea_config.resolve_profile()
        msg = str(ctx.exception)
        self.assertIn("ghost", msg)
        self.assertIn("prgs", msg)

    def test_config_set_but_profile_unset_raises(self):
        with patch.dict(os.environ, {"GITEA_MCP_CONFIG": self.path}, clear=True):
            with self.assertRaises(gitea_config.ConfigError) as ctx:
                gitea_config.resolve_profile()
        self.assertIn("GITEA_MCP_PROFILE", str(ctx.exception))

    def test_bad_top_level_shape_raises(self):
        self._write({"version": 1, "nope": 1})
        with patch.dict(os.environ, self._env("prgs"), clear=True):
            with self.assertRaises(gitea_config.ConfigError):
                gitea_config.resolve_profile()

    def test_unsupported_version_raises(self):
        self._write({"version": 2, "profiles": {"prgs": {}}})
        with patch.dict(os.environ, self._env("prgs"), clear=True):
            with self.assertRaises(gitea_config.ConfigError) as ctx:
                gitea_config.resolve_profile()
        self.assertIn("version", str(ctx.exception))

    def test_missing_version_fails_closed(self):
        # Changed by #103: an unversioned config is ambiguous between the v1
        # and v2 shapes, so the loader now refuses to guess.
        self._write({"profiles": {"prgs": {"base_url": "https://x"}}})
        with patch.dict(os.environ, self._env("prgs"), clear=True):
            with self.assertRaises(gitea_config.ConfigError) as ctx:
                gitea_config.resolve_profile()
        self.assertIn("version", str(ctx.exception))


# ---------------------------------------------------------------------------
# Auth reference parsing + resolution
# ---------------------------------------------------------------------------
class TestAuthReferences(_ConfigBase):

    def test_keychain_reference_parses(self):
        with patch.dict(os.environ, self._env("prgs"), clear=True):
            p = gitea_config.resolve_profile()
        self.assertEqual(p["auth"], {"type": "keychain", "id": "prgs-gitea-token"})
        self.assertEqual(gitea_config.auth_source_name(p), "keychain:prgs-gitea-token")

    def test_env_reference_parses(self):
        with patch.dict(os.environ, self._env("mdcps-env"), clear=True):
            p = gitea_config.resolve_profile()
        self.assertEqual(gitea_config.auth_source_name(p), "GITEA_TOKEN_MDCPS")

    def test_keychain_token_resolved_via_injected_lookup(self):
        with patch.dict(os.environ, self._env("prgs"), clear=True):
            p = gitea_config.resolve_profile()
            token = gitea_config.resolve_token(
                p, keychain_lookup=lambda i: "kc-token" if i == "prgs-gitea-token" else None)
        self.assertEqual(token, "kc-token")

    def test_env_token_resolved_from_env(self):
        env = self._env("mdcps-env", GITEA_TOKEN_MDCPS="mdcps-token-value")
        with patch.dict(os.environ, env, clear=True):
            p = gitea_config.resolve_profile()
            self.assertEqual(gitea_config.resolve_token(p), "mdcps-token-value")

    def test_keychain_lookup_uses_security_binary(self):
        # _keychain_token shells out to `security find-generic-password`.
        class _Proc:
            returncode = 0
            stdout = "secret-from-keychain\n"
        with patch("gitea_config.subprocess.run", return_value=_Proc()) as run:
            self.assertEqual(gitea_config._keychain_token("some-id"), "secret-from-keychain")
        args = run.call_args[0][0]
        self.assertEqual(args[0], "security")
        self.assertIn("some-id", args)

    def test_missing_env_secret_raises_clearly(self):
        with patch.dict(os.environ, self._env("mdcps-env"), clear=True):
            p = gitea_config.resolve_profile()
            with self.assertRaises(gitea_config.ConfigError) as ctx:
                gitea_config.resolve_token(p)
        self.assertIn("GITEA_TOKEN_MDCPS", str(ctx.exception))

    def test_missing_keychain_secret_raises_clearly(self):
        with patch.dict(os.environ, self._env("prgs"), clear=True):
            p = gitea_config.resolve_profile()
            with self.assertRaises(gitea_config.ConfigError) as ctx:
                gitea_config.resolve_token(p, keychain_lookup=lambda i: None)
        self.assertIn("prgs-gitea-token", str(ctx.exception))

    def test_invalid_auth_type_rejected(self):
        self._write({"version": 1, "profiles": {"p": {"auth": {"type": "vault"}}}})
        with patch.dict(os.environ, self._env("p"), clear=True):
            with self.assertRaises(gitea_config.ConfigError) as ctx:
                gitea_config.resolve_profile()
        self.assertIn("invalid auth type", str(ctx.exception))

    def test_keychain_without_id_rejected(self):
        self._write({"version": 1, "profiles": {"p": {"auth": {"type": "keychain"}}}})
        with patch.dict(os.environ, self._env("p"), clear=True):
            with self.assertRaises(gitea_config.ConfigError):
                gitea_config.resolve_profile()


# ---------------------------------------------------------------------------
# Secret redaction
# ---------------------------------------------------------------------------
class TestRedaction(_ConfigBase):

    def test_inline_token_rejected_without_echo(self):
        self._write({"version": 1, "profiles": {"prgs": {"token": "super-secret-token"}}})
        with patch.dict(os.environ, self._env("prgs"), clear=True):
            with self.assertRaises(gitea_config.ConfigError) as ctx:
                gitea_config.resolve_profile()
        msg = str(ctx.exception)
        self.assertIn("auth", msg)
        self.assertNotIn("super-secret-token", msg)

    def test_inline_password_rejected_without_echo(self):
        self._write({"version": 1, "profiles": {"prgs": {"password": "hunter2secret"}}})
        with patch.dict(os.environ, self._env("prgs"), clear=True):
            with self.assertRaises(gitea_config.ConfigError) as ctx:
                gitea_config.resolve_profile()
        self.assertNotIn("hunter2secret", str(ctx.exception))

    def test_profile_metadata_has_no_token_value(self):
        env = self._env("mdcps-env", GITEA_TOKEN_MDCPS="mdcps-token-value")
        with patch.dict(os.environ, env, clear=True):
            p = gitea_config.resolve_profile()
        self.assertNotIn("mdcps-token-value", json.dumps(p))


# ---------------------------------------------------------------------------
# gitea_auth integration
# ---------------------------------------------------------------------------
class TestAuthIntegration(_ConfigBase):

    def test_legacy_env_only_profile_unchanged(self):
        with patch.dict(os.environ, {"GITEA_PROFILE_NAME": "gitea-reviewer",
                                     "GITEA_ALLOWED_OPERATIONS": "read,review"},
                        clear=True):
            p = gitea_auth.get_profile()
        self.assertEqual(p["profile_name"], "gitea-reviewer")
        self.assertEqual(p["allowed_operations"], ["read", "review"])
        self.assertIsNone(p["base_url"])

    def test_json_fills_profile_when_env_absent(self):
        with patch.dict(os.environ, self._env("prgs"), clear=True):
            p = gitea_auth.get_profile()
        self.assertEqual(p["profile_name"], "personal-prgs")  # from execution_profile
        self.assertEqual(p["base_url"], "https://gitea.prgs.cc")
        self.assertEqual(p["username"], "jcwalker3")
        self.assertEqual(p["default_owner"], "Scaled-Tech-Consulting")
        self.assertEqual(p["token_source_name"], "keychain:prgs-gitea-token")

    def test_env_overrides_json(self):
        env = self._env("prgs",
                        GITEA_PROFILE_NAME="env-name",
                        GITEA_BASE_URL="https://env.example")
        with patch.dict(os.environ, env, clear=True):
            p = gitea_auth.get_profile()
        self.assertEqual(p["profile_name"], "env-name")
        self.assertEqual(p["base_url"], "https://env.example")

    def test_get_profile_propagates_config_error(self):
        with patch.dict(os.environ, self._env("ghost"), clear=True):
            with self.assertRaises(gitea_config.ConfigError):
                gitea_auth.get_profile()

    def test_auth_header_uses_json_env_token(self):
        env = self._env("mdcps-env", GITEA_TOKEN_MDCPS="json-env-token")
        with patch.dict(os.environ, env, clear=True):
            header = gitea_auth.get_auth_header("gitea.example.com")
        self.assertEqual(header, "token json-env-token")

    def test_explicit_env_token_overrides_json(self):
        env = self._env("mdcps-env", GITEA_TOKEN_MDCPS="json-env-token",
                        GITEA_TOKEN="process-token")
        with patch.dict(os.environ, env, clear=True):
            header = gitea_auth.get_auth_header("gitea.example.com")
        self.assertEqual(header, "token process-token")

    def test_auth_header_unresolvable_ref_fails_closed(self):
        # env token ref points at an unset var -> with GITEA_MCP_CONFIG set the
        # ConfigError propagates (fail closed, #120): no silent fallback to
        # Basic auth or another credential source.
        with patch.dict(os.environ, self._env("mdcps-env"), clear=True):
            with patch("gitea_auth.get_credentials", return_value=("", "")):
                with self.assertRaises(gitea_config.ConfigError):
                    gitea_auth.get_auth_header("gitea.example.com")


# ---------------------------------------------------------------------------
# No network during config parsing
# ---------------------------------------------------------------------------
class TestNoNetwork(_ConfigBase):

    def test_config_load_makes_no_network_calls(self):
        boom = AssertionError("config parsing must not touch the network")
        with patch.dict(os.environ, self._env("prgs"), clear=True):
            with patch("gitea_auth.urllib.request.urlopen", side_effect=boom):
                self.assertIsNotNone(gitea_config.resolve_profile())
                self.assertEqual(
                    gitea_auth.get_profile()["profile_name"], "personal-prgs")


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