"""E2E tests for the /api/skills endpoint.

Spins up a real AMC server on a random port and verifies the skills API
returns correct data for Claude and Codex agents, including trigger
characters, alphabetical sorting, and response format.
"""

import json
import socket
import tempfile
import threading
import time
import unittest
import urllib.request
from http.server import ThreadingHTTPServer
from pathlib import Path
from unittest.mock import patch

from amc_server.handler import AMCHandler


def _find_free_port():
    """Find an available port for the test server."""
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind(("127.0.0.1", 0))
        return s.getsockname()[1]


def _get_json(url):
    """Fetch JSON from a URL, returning (status_code, parsed_json)."""
    req = urllib.request.Request(url)
    try:
        with urllib.request.urlopen(req, timeout=5) as resp:
            return resp.status, json.loads(resp.read())
    except urllib.error.HTTPError as e:
        return e.code, json.loads(e.read())


class TestSkillsEndpointE2E(unittest.TestCase):
    """E2E tests: start a real server and hit /api/skills over HTTP."""

    @classmethod
    def setUpClass(cls):
        """Start a test server on a random port with mock skill data."""
        cls.port = _find_free_port()
        cls.base_url = f"http://127.0.0.1:{cls.port}"

        # Create temp directories for skill data
        cls.tmpdir = tempfile.mkdtemp()
        cls.home = Path(cls.tmpdir)

        # Claude skills
        for name, desc in [
            ("commit", "Create a git commit"),
            ("review-pr", "Review a pull request"),
            ("comment", "Add a comment"),
        ]:
            skill_dir = cls.home / ".claude/skills" / name
            skill_dir.mkdir(parents=True, exist_ok=True)
            (skill_dir / "SKILL.md").write_text(desc)

        # Codex curated skills
        cache_dir = cls.home / ".codex/vendor_imports"
        cache_dir.mkdir(parents=True, exist_ok=True)
        cache = {
            "skills": [
                {"id": "lint", "shortDescription": "Lint code"},
                {"id": "deploy", "shortDescription": "Deploy to prod"},
            ]
        }
        (cache_dir / "skills-curated-cache.json").write_text(json.dumps(cache))

        # Codex user skill
        codex_skill = cls.home / ".codex/skills/my-script"
        codex_skill.mkdir(parents=True, exist_ok=True)
        (codex_skill / "SKILL.md").write_text("Run my custom script")

        # Patch Path.home() for the skills enumeration
        cls.home_patcher = patch.object(Path, "home", return_value=cls.home)
        cls.home_patcher.start()

        # Start server in background thread
        cls.server = ThreadingHTTPServer(("127.0.0.1", cls.port), AMCHandler)
        cls.server_thread = threading.Thread(target=cls.server.serve_forever)
        cls.server_thread.daemon = True
        cls.server_thread.start()

        # Wait for server to be ready
        for _ in range(50):
            try:
                with socket.create_connection(("127.0.0.1", cls.port), timeout=0.1):
                    break
            except OSError:
                time.sleep(0.05)

    @classmethod
    def tearDownClass(cls):
        """Shut down the test server."""
        cls.server.shutdown()
        cls.server_thread.join(timeout=5)
        cls.home_patcher.stop()

    # -- Core: /api/skills serves correctly --

    def test_skills_default_is_claude(self):
        """GET /api/skills without ?agent defaults to claude (/ trigger)."""
        status, data = _get_json(f"{self.base_url}/api/skills")
        self.assertEqual(status, 200)
        self.assertEqual(data["trigger"], "/")
        self.assertIsInstance(data["skills"], list)

    def test_claude_skills_returned(self):
        """GET /api/skills?agent=claude returns Claude skills."""
        status, data = _get_json(f"{self.base_url}/api/skills?agent=claude")
        self.assertEqual(status, 200)
        self.assertEqual(data["trigger"], "/")
        names = [s["name"] for s in data["skills"]]
        self.assertIn("commit", names)
        self.assertIn("review-pr", names)
        self.assertIn("comment", names)

    def test_codex_skills_returned(self):
        """GET /api/skills?agent=codex returns Codex skills with $ trigger."""
        status, data = _get_json(f"{self.base_url}/api/skills?agent=codex")
        self.assertEqual(status, 200)
        self.assertEqual(data["trigger"], "$")
        names = [s["name"] for s in data["skills"]]
        self.assertIn("lint", names)
        self.assertIn("deploy", names)
        self.assertIn("my-script", names)

    def test_unknown_agent_defaults_to_claude(self):
        """Unknown agent type defaults to claude behavior."""
        status, data = _get_json(f"{self.base_url}/api/skills?agent=unknown-agent")
        self.assertEqual(status, 200)
        self.assertEqual(data["trigger"], "/")

    # -- Response format --

    def test_response_has_trigger_and_skills_keys(self):
        """Response JSON has exactly trigger and skills keys."""
        _, data = _get_json(f"{self.base_url}/api/skills?agent=claude")
        self.assertIn("trigger", data)
        self.assertIn("skills", data)

    def test_each_skill_has_name_and_description(self):
        """Each skill object has name and description fields."""
        _, data = _get_json(f"{self.base_url}/api/skills?agent=claude")
        for skill in data["skills"]:
            self.assertIn("name", skill)
            self.assertIn("description", skill)
            self.assertIsInstance(skill["name"], str)
            self.assertIsInstance(skill["description"], str)

    # -- Alphabetical sorting --

    def test_claude_skills_alphabetically_sorted(self):
        """Claude skills are returned in alphabetical order."""
        _, data = _get_json(f"{self.base_url}/api/skills?agent=claude")
        names = [s["name"] for s in data["skills"]]
        self.assertEqual(names, sorted(names, key=str.lower))

    def test_codex_skills_alphabetically_sorted(self):
        """Codex skills are returned in alphabetical order."""
        _, data = _get_json(f"{self.base_url}/api/skills?agent=codex")
        names = [s["name"] for s in data["skills"]]
        self.assertEqual(names, sorted(names, key=str.lower))

    # -- Descriptions --

    def test_claude_skill_descriptions(self):
        """Claude skills have correct descriptions from SKILL.md."""
        _, data = _get_json(f"{self.base_url}/api/skills?agent=claude")
        by_name = {s["name"]: s["description"] for s in data["skills"]}
        self.assertEqual(by_name["commit"], "Create a git commit")
        self.assertEqual(by_name["review-pr"], "Review a pull request")

    def test_codex_curated_descriptions(self):
        """Codex curated skills have correct descriptions from cache."""
        _, data = _get_json(f"{self.base_url}/api/skills?agent=codex")
        by_name = {s["name"]: s["description"] for s in data["skills"]}
        self.assertEqual(by_name["lint"], "Lint code")
        self.assertEqual(by_name["deploy"], "Deploy to prod")

    def test_codex_user_skill_description(self):
        """Codex user-installed skills have descriptions from SKILL.md."""
        _, data = _get_json(f"{self.base_url}/api/skills?agent=codex")
        by_name = {s["name"]: s["description"] for s in data["skills"]}
        self.assertEqual(by_name["my-script"], "Run my custom script")

    # -- CORS --

    def test_cors_header_present(self):
        """Response includes Access-Control-Allow-Origin header."""
        url = f"{self.base_url}/api/skills?agent=claude"
        with urllib.request.urlopen(url, timeout=5) as resp:
            cors = resp.headers.get("Access-Control-Allow-Origin")
        self.assertEqual(cors, "*")


class TestSkillsEndpointEmptyE2E(unittest.TestCase):
    """E2E tests: server with no skills data."""

    @classmethod
    def setUpClass(cls):
        cls.port = _find_free_port()
        cls.base_url = f"http://127.0.0.1:{cls.port}"

        # Empty home directory - no skills at all
        cls.tmpdir = tempfile.mkdtemp()
        cls.home = Path(cls.tmpdir)

        cls.home_patcher = patch.object(Path, "home", return_value=cls.home)
        cls.home_patcher.start()

        cls.server = ThreadingHTTPServer(("127.0.0.1", cls.port), AMCHandler)
        cls.server_thread = threading.Thread(target=cls.server.serve_forever)
        cls.server_thread.daemon = True
        cls.server_thread.start()

        for _ in range(50):
            try:
                with socket.create_connection(("127.0.0.1", cls.port), timeout=0.1):
                    break
            except OSError:
                time.sleep(0.05)

    @classmethod
    def tearDownClass(cls):
        cls.server.shutdown()
        cls.server_thread.join(timeout=5)
        cls.home_patcher.stop()

    def test_empty_claude_skills(self):
        """Server with no Claude skills returns empty list."""
        status, data = _get_json(f"{self.base_url}/api/skills?agent=claude")
        self.assertEqual(status, 200)
        self.assertEqual(data["trigger"], "/")
        self.assertEqual(data["skills"], [])

    def test_empty_codex_skills(self):
        """Server with no Codex skills returns empty list."""
        status, data = _get_json(f"{self.base_url}/api/skills?agent=codex")
        self.assertEqual(status, 200)
        self.assertEqual(data["trigger"], "$")
        self.assertEqual(data["skills"], [])


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