#!/usr/bin/env python3
"""Temp-HOME regression for agent-client/pi/install.sh.

Executes the real installer against an isolated temporary HOME and asserts
that Seulgivibe Codex remains api: openai-responses, that existing user
settings / API keys / dev-corp data and timeout semantics are preserved,
that idempotent re-runs are safe, and that no real user home or credential
is touched.  Fake keys only — no network access required.
"""

import json
import os
import shutil
import subprocess
import tempfile
import unittest
from unittest import mock
from pathlib import Path


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _installer_script() -> Path:
    """Return the absolute path to the real install.sh we are testing."""
    return Path(__file__).resolve().parent / "install.sh"


def _run_installer(temp_home: Path) -> subprocess.CompletedProcess:
    """Invoke install.sh with HOME pointed at temp_home."""
    env = os.environ.copy()
    env["HOME"] = str(temp_home)
    env.pop("PI_HOME", None)  # ensure no override leaks in
    result = subprocess.run(
        ["bash", str(_installer_script())],
        env=env,
        capture_output=True,
        text=True,
        timeout=30,
    )
    return result


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

class _TempHomeMixin:
    """Common setUp / tearDown for temp-home tests."""

    def setUp(self):
        self.tmp_home = Path(tempfile.mkdtemp(prefix="pi-regression-"))
        self.pi_dir = self.tmp_home / ".pi" / "agent"
        self.pi_dir.mkdir(parents=True)
        self.settings_path = self.pi_dir / "settings.json"
        self.models_path = self.pi_dir / "models.json"

    def tearDown(self):
        shutil.rmtree(self.tmp_home, ignore_errors=True)


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

class TestPreservesOpenaiResponsesConsumptionAndUserSettings(_TempHomeMixin, unittest.TestCase):
    """Verify the consumer contract: api: openai-responses, defaults, keys, timeout."""

    def setUp(self):
        super().setUp()
        self.expected_dev_corp = {
            "baseUrl": "https://dev.corp.example.com/v1",
            "api": "openai-chat",
            "apiKey": "sk-DEV-CORP-KEY",
            "models": [{"id": "dev-model-1", "name": "Dev Corp Model"}],
        }

        # Seed representative existing user data.
        # Custom defaultProvider/defaultModel are seeded to prove the installer
        # does NOT overwrite them with setdefault.
        self.settings_path.write_text(json.dumps({
            "defaultProvider": "anthropic/claude-3-opus:high",  # custom — must be preserved
            "defaultModel": "claude-3-opus-20240401",            # custom — must be preserved
            "defaultThinkingLevel": "medium",        # user preference — must be preserved
            "hideThinkingBlock": True,               # user preference
            "httpIdleTimeoutMs": 300000,             # user value — must be overridden to 0
            "retry": {
                "enabled": True,                    # must be disabled
                "maxRetries": 3,                    # must be overridden to 0
                "provider": {
                    "timeoutMs": 120000,             # must be removed
                    "maxRetries": 3,                 # must be overridden to 0
                    "maxRetryDelayMs": 30000,        # must be overridden
                }
            },
            "extensions": [
                "extensions/openai-sampling-parameters.ts",  # must be removed
                "extensions/custom-tool.ts",                   # must be preserved
            ],
            "enabledModels": [
                "seulgivibe-openai/gpt-5.1:medium",  # legacy — must be removed
                "seulgivibe-openai/gpt-5.5:xhigh",   # legacy — must be removed
                "anthropic/claude-3-opus:high",       # third-party — must be preserved
            ],
            "customTheme": "dark-plus",              # arbitrary user data — must be preserved
        }, indent=2))

        self.models_path.write_text(json.dumps({
            "providers": {
                "seulgivibe-openai": {
                    "baseUrl": "https://old.example.com/openai/v1",
                    "api": "openai-chat",
                    "apiKey": "sk-OLD-USER-KEY",  # must be preserved
                    "models": [
                        {"id": "gpt-5.1", "name": "Old GPT-5.1"},
                    ],
                },
                "dev-corp": {
                    **self.expected_dev_corp,
                },
            },
        }, indent=2))

    def test_preserves_openai_responses_consumption_and_user_settings(self):
        result = _run_installer(self.tmp_home)
        self.assertEqual(result.returncode, 0, msg=f"stderr: {result.stderr}")

        settings = json.loads(self.settings_path.read_text())
        models = json.loads(self.models_path.read_text())

        # 1) Seulgivibe Codex provider exists with api: openai-responses
        codex = models["providers"]["seulgivibe-codex"]
        self.assertEqual(codex["api"], "openai-responses")
        self.assertEqual(codex["baseUrl"], "https://seulgivibe.lgudax.cool/openai/v1")

        # 2) Custom defaultProvider is preserved (installer uses setdefault)
        self.assertEqual(settings.get("defaultProvider"), "anthropic/claude-3-opus:high")

        # 2b) Custom defaultModel is preserved (installer uses setdefault)
        self.assertEqual(settings.get("defaultModel"), "claude-3-opus-20240401")

        # 4) User's thinking level preference is preserved
        self.assertEqual(settings["defaultThinkingLevel"], "medium")

        # 5) User's hideThinkingBlock is preserved
        self.assertEqual(settings["hideThinkingBlock"], True)

        # 6) HTTP idle timeout is disabled (0)
        self.assertEqual(settings["httpIdleTimeoutMs"], 0)

        # 7) Provider timeout override is removed
        provider_retry = settings["retry"]["provider"]
        self.assertNotIn("timeoutMs", provider_retry)

        # 8) Retry policy is set
        self.assertEqual(settings["retry"]["enabled"], False)
        self.assertEqual(settings["retry"]["maxRetries"], 0)
        self.assertEqual(provider_retry["maxRetries"], 0)
        self.assertEqual(provider_retry["maxRetryDelayMs"], 60000)

        # 9) Sampling extension is removed; other extensions preserved
        self.assertNotIn("extensions/openai-sampling-parameters.ts", settings["extensions"])
        self.assertIn("extensions/custom-tool.ts", settings["extensions"])

        # 10) Legacy seulgivibe-openai entries removed from enabledModels
        enabled = settings["enabledModels"]
        for m in enabled:
            self.assertFalse(m.startswith("seulgivibe-openai/"), f"legacy entry not removed: {m}")
        # Third-party model preserved
        self.assertIn("anthropic/claude-3-opus:high", enabled)

        # 11) Custom user data preserved
        self.assertEqual(settings["customTheme"], "dark-plus")

        # 12) User API key preserved on codex provider
        self.assertEqual(codex["apiKey"], "sk-OLD-USER-KEY")

        # 13) Legacy seulgivibe-openai provider removed from models
        self.assertNotIn("seulgivibe-openai", models["providers"])

        # 14) dev-corp provider preserved — full document equality
        dev = models["providers"]["dev-corp"]
        self.assertEqual(dev, self.expected_dev_corp)

        # 15) Codex models include expected entries
        codex_model_ids = [m["id"] for m in codex.get("models", [])]
        self.assertIn("gpt-5.1", codex_model_ids)
        self.assertIn("gpt-5.5", codex_model_ids)

        # 16) Legacy openai provider entry in enabledModels is removed
        #      (already checked above but be explicit about the contract)
        legacy_in_enabled = [m for m in enabled if m.startswith("seulgivibe-openai/")]
        self.assertEqual(legacy_in_enabled, [], "Legacy openai entries must be removed from enabledModels")


class TestIdempotentRerun(_TempHomeMixin, unittest.TestCase):
    """Re-running the installer must be safe and produce stable output."""

    def setUp(self):
        super().setUp()
        self.settings_path.write_text(json.dumps({
            "defaultProvider": "seulgivibe-codex",
            "defaultModel": "gpt-5.5",
            "customSetting": "keep-me",
        }, indent=2))
        self.models_path.write_text(json.dumps({
            "providers": {
                "seulgivibe-codex": {
                    "baseUrl": "https://seulgivibe.lgudax.cool/openai/v1",
                    "api": "openai-responses",
                    "apiKey": "sk-EXISTING",
                    "models": [
                        {"id": "gpt-5.5", "name": "Seulgivibe Codex GPT-5.5", "input": ["text"]},
                    ],
                },
            },
        }, indent=2))

    def test_is_idempotent(self):
        # First run
        r1 = _run_installer(self.tmp_home)
        self.assertEqual(r1.returncode, 0, msg=f"stderr: {r1.stderr}")

        # Capture complete parsed documents after first run.
        settings_1 = json.loads(self.settings_path.read_text())
        models_1 = json.loads(self.models_path.read_text())

        # Second run (idempotent)
        r2 = _run_installer(self.tmp_home)
        self.assertEqual(r2.returncode, 0, msg=f"stderr: {r2.stderr}")

        # Compare complete parsed documents across reruns.
        settings_2 = json.loads(self.settings_path.read_text())
        models_2 = json.loads(self.models_path.read_text())
        self.assertEqual(settings_1, settings_2, "settings.json drifted across reruns")
        self.assertEqual(models_1, models_2, "models.json drifted across reruns")

        # Backup files were created (proves the installer ran and wrote)
        bak_files = list(self.pi_dir.glob("*.bak-*"))
        self.assertGreater(len(bak_files), 0, "Expected backup files from the second run")


class TestNoUserHomeTouch(_TempHomeMixin, unittest.TestCase):
    """Confirm the installer does not read or write outside the temp HOME."""

    def test_installer_uses_only_test_home(self):
        # Two distinct test-owned homes: decoy_home receives HOME override,
        # installer_home is the actual target the installer writes to.
        decoy_home = self.tmp_home / "decoy-home"
        installer_home = self.tmp_home / "installer-home"
        decoy_home.mkdir()
        installer_home.mkdir()

        # Seed only test-owned sentinel data in decoy_home.
        decoy_settings = decoy_home / ".pi" / "agent" / "settings.json"
        decoy_models = decoy_home / ".pi" / "agent" / "models.json"
        sentinel = b"DECOY-SENTINEL-BYTES-DO-NOT-OVERWRITE"
        decoy_settings.parent.mkdir(parents=True)
        decoy_settings.write_bytes(sentinel)
        # Leave decoy_models absent to prove absence state is preserved.

        # Run installer with HOME pointing at decoy_home but installer writes
        # to installer_home via the env HOME override.
        with mock.patch.dict(os.environ, {"HOME": str(decoy_home)}):
            result = _run_installer(installer_home)
        self.assertEqual(result.returncode, 0, msg=f"stderr: {result.stderr}")

        # Decoy home must remain untouched: sentinel bytes intact, absence preserved.
        self.assertTrue(decoy_settings.exists(), "Decoy settings.json should still exist")
        self.assertEqual(
            decoy_settings.read_bytes(), sentinel,
            "Decoy settings.json was modified by the installer!"
        )
        self.assertFalse(
            decoy_models.exists(),
            "Decoy models.json (absent) should remain absent",
        )


class TestFakeKeysOnly(_TempHomeMixin, unittest.TestCase):
    """Ensure no real credentials appear in the temp HOME after install."""

    def test_no_real_credentials(self):
        # Seed with a fake key only.
        self.models_path.write_text(json.dumps({
            "providers": {
                "seulgivibe-codex": {
                    "apiKey": "sk-FAKE-TEST-KEY",
                },
            },
        }, indent=2))

        result = _run_installer(self.tmp_home)
        self.assertEqual(result.returncode, 0, msg=f"stderr: {result.stderr}")

        models = json.loads(self.models_path.read_text())
        codex_key = models["providers"]["seulgivibe-codex"].get("apiKey", "")

        # Must not contain any real-looking key patterns.
        self.assertNotIn("sk-live-", codex_key)
        self.assertNotIn("sk-proj-", codex_key)
        self.assertNotIn("sk-ant-", codex_key)
        # Should retain the fake key we seeded.
        self.assertEqual(codex_key, "sk-FAKE-TEST-KEY")


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