"""Integration tests for SmartTools Registry.

These tests verify the CLI and server work together correctly.
Run with: pytest tests/test_registry_integration.py -v

Note: These tests require the registry server to be running locally:
    python -m smarttools.registry.app
"""

import json
import os
import shutil
import tempfile
from pathlib import Path
from unittest.mock import patch, MagicMock

import pytest
import yaml

# Test without requiring server for unit tests
from smarttools.config import Config, RegistryConfig, load_config, save_config
from smarttools.manifest import (
    Manifest, Dependency, ToolOverride,
    load_manifest, save_manifest, create_manifest,
    parse_version_constraint
)
from smarttools.resolver import (
    ToolSpec, ToolResolver, ResolvedTool,
    resolve_tool, find_tool, list_installed_tools
)
from smarttools.registry_client import (
    RegistryClient, RegistryError, PaginatedResponse,
    ToolInfo, DownloadResult
)


class TestToolSpec:
    """Tests for ToolSpec parsing."""

    def test_parse_simple_name(self):
        spec = ToolSpec.parse("summarize")
        assert spec.owner is None
        assert spec.name == "summarize"
        assert spec.version is None

    def test_parse_qualified_name(self):
        spec = ToolSpec.parse("rob/summarize")
        assert spec.owner == "rob"
        assert spec.name == "summarize"
        assert spec.version is None

    def test_parse_with_version(self):
        spec = ToolSpec.parse("rob/summarize@1.0.0")
        assert spec.owner == "rob"
        assert spec.name == "summarize"
        assert spec.version == "1.0.0"

    def test_parse_constraint_version(self):
        spec = ToolSpec.parse("summarize@^1.0.0")
        assert spec.owner is None
        assert spec.name == "summarize"
        assert spec.version == "^1.0.0"

    def test_full_name_qualified(self):
        spec = ToolSpec.parse("rob/summarize")
        assert spec.full_name == "rob/summarize"

    def test_full_name_unqualified(self):
        spec = ToolSpec.parse("summarize")
        assert spec.full_name == "summarize"


class TestManifest:
    """Tests for project manifest handling."""

    def test_create_manifest(self):
        manifest = Manifest(name="test-project", version="1.0.0")
        assert manifest.name == "test-project"
        assert manifest.version == "1.0.0"
        assert manifest.dependencies == []

    def test_add_dependency(self):
        manifest = Manifest()
        manifest.add_dependency("rob/summarize", "^1.0.0")
        assert len(manifest.dependencies) == 1
        assert manifest.dependencies[0].name == "rob/summarize"
        assert manifest.dependencies[0].version == "^1.0.0"

    def test_add_duplicate_dependency_updates(self):
        manifest = Manifest()
        manifest.add_dependency("rob/summarize", "^1.0.0")
        manifest.add_dependency("rob/summarize", "^2.0.0")
        assert len(manifest.dependencies) == 1
        assert manifest.dependencies[0].version == "^2.0.0"

    def test_get_override(self):
        manifest = Manifest(
            overrides={"rob/summarize": ToolOverride(provider="ollama")}
        )
        override = manifest.get_override("rob/summarize")
        assert override is not None
        assert override.provider == "ollama"

    def test_get_override_short_name(self):
        manifest = Manifest(
            overrides={"rob/summarize": ToolOverride(provider="ollama")}
        )
        # Should match by short name
        override = manifest.get_override("summarize")
        assert override is not None
        assert override.provider == "ollama"

    def test_to_dict_roundtrip(self):
        manifest = Manifest(
            name="test",
            version="2.0.0",
            dependencies=[Dependency("rob/summarize", "^1.0.0")],
            overrides={"rob/summarize": ToolOverride(provider="claude")}
        )
        data = manifest.to_dict()
        restored = Manifest.from_dict(data)
        assert restored.name == manifest.name
        assert restored.version == manifest.version
        assert len(restored.dependencies) == 1
        assert restored.dependencies[0].name == "rob/summarize"


class TestVersionConstraint:
    """Tests for version constraint parsing."""

    def test_exact_version(self):
        result = parse_version_constraint("1.2.3")
        assert result["operator"] == "="
        assert result["version"] == "1.2.3"

    def test_any_version(self):
        result = parse_version_constraint("*")
        assert result["operator"] == "*"
        assert result["version"] is None

    def test_caret_constraint(self):
        result = parse_version_constraint("^1.2.3")
        assert result["operator"] == "^"
        assert result["version"] == "1.2.3"

    def test_tilde_constraint(self):
        result = parse_version_constraint("~1.2.3")
        assert result["operator"] == "~"
        assert result["version"] == "1.2.3"

    def test_gte_constraint(self):
        result = parse_version_constraint(">=1.0.0")
        assert result["operator"] == ">="
        assert result["version"] == "1.0.0"


class TestDependency:
    """Tests for Dependency parsing."""

    def test_from_dict_object(self):
        dep = Dependency.from_dict({"name": "rob/tool", "version": "^1.0.0"})
        assert dep.name == "rob/tool"
        assert dep.version == "^1.0.0"

    def test_from_dict_string_simple(self):
        dep = Dependency.from_dict("rob/tool")
        assert dep.name == "rob/tool"
        assert dep.version == "*"

    def test_from_dict_string_with_version(self):
        dep = Dependency.from_dict("rob/tool@^2.0.0")
        assert dep.name == "rob/tool"
        assert dep.version == "^2.0.0"

    def test_owner_property(self):
        dep = Dependency(name="rob/summarize")
        assert dep.owner == "rob"

    def test_tool_name_property(self):
        dep = Dependency(name="rob/summarize")
        assert dep.tool_name == "summarize"


class TestConfig:
    """Tests for configuration handling."""

    def test_default_config(self):
        config = Config()
        assert config.registry.url == "https://gitea.brrd.tech/api/v1"
        assert config.auto_fetch_from_registry is True
        assert config.client_id.startswith("anon_")

    def test_config_to_dict_roundtrip(self):
        config = Config(
            registry=RegistryConfig(token="test_token"),
            auto_fetch_from_registry=False,
            default_provider="claude"
        )
        data = config.to_dict()
        restored = Config.from_dict(data)
        assert restored.registry.token == "test_token"
        assert restored.auto_fetch_from_registry is False
        assert restored.default_provider == "claude"


class TestRegistryClient:
    """Tests for the registry client (mocked)."""

    def test_tool_info_from_dict(self):
        data = {
            "owner": "rob",
            "name": "summarize",
            "version": "1.0.0",
            "description": "Test tool",
            "downloads": 100
        }
        info = ToolInfo.from_dict(data)
        assert info.owner == "rob"
        assert info.name == "summarize"
        assert info.full_name == "rob/summarize"
        assert info.downloads == 100

    def test_paginated_response(self):
        response = PaginatedResponse(
            data=[{"name": "tool1"}, {"name": "tool2"}],
            page=1,
            per_page=20,
            total=2,
            total_pages=1
        )
        assert len(response.data) == 2
        assert response.total == 2


class TestToolResolver:
    """Tests for tool resolution."""

    def test_deterministic_owner_order(self, tmp_path):
        """Test that official namespace is preferred over others."""
        # Create fake tool directories
        tools_dir = tmp_path / ".smarttools"

        # Create alice/mytool
        alice_tool = tools_dir / "alice" / "mytool"
        alice_tool.mkdir(parents=True)
        (alice_tool / "config.yaml").write_text(yaml.dump({
            "name": "mytool",
            "description": "Alice's version"
        }))

        # Create official/mytool
        official_tool = tools_dir / "official" / "mytool"
        official_tool.mkdir(parents=True)
        (official_tool / "config.yaml").write_text(yaml.dump({
            "name": "mytool",
            "description": "Official version"
        }))

        # Create zebra/mytool (should come after official alphabetically)
        zebra_tool = tools_dir / "zebra" / "mytool"
        zebra_tool.mkdir(parents=True)
        (zebra_tool / "config.yaml").write_text(yaml.dump({
            "name": "mytool",
            "description": "Zebra's version"
        }))

        # Test resolution prefers official
        resolver = ToolResolver(project_dir=tmp_path, auto_fetch=False)
        result = resolver._find_in_local(ToolSpec.parse("mytool"), [])

        assert result is not None
        assert result.owner == "official"
        assert result.tool.description == "Official version"


# Integration tests (require server)
@pytest.mark.integration
class TestRegistryIntegration:
    """Integration tests requiring a running registry server.

    Run with: pytest tests/test_registry_integration.py -v -m integration
    """

    @pytest.fixture
    def client(self):
        return RegistryClient(base_url="http://localhost:5000/api/v1")

    def test_list_tools(self, client):
        """Test listing tools from registry."""
        result = client.list_tools(per_page=5)
        assert isinstance(result, PaginatedResponse)
        # May be empty if no tools seeded

    def test_search_tools(self, client):
        """Test searching for tools."""
        result = client.search_tools("test", per_page=5)
        assert isinstance(result, PaginatedResponse)

    def test_get_categories(self, client):
        """Test getting categories."""
        categories = client.get_categories()
        assert isinstance(categories, list)

    def test_get_index(self, client):
        """Test getting full index."""
        index = client.get_index(force_refresh=True)
        assert "tools" in index
        assert "tool_count" in index


@pytest.mark.integration
class TestAuthIntegration:
    """Integration tests for authentication endpoints.

    Run with: pytest tests/test_registry_integration.py -v -m integration
    """

    @pytest.fixture
    def base_url(self):
        return "http://localhost:5000/api/v1"

    @pytest.fixture
    def session(self):
        import requests
        return requests.Session()

    def test_register_validation(self, session, base_url):
        """Test registration validation errors."""
        # Missing fields
        resp = session.post(f"{base_url}/register", json={})
        assert resp.status_code == 400
        data = resp.json()
        assert data["error"]["code"] == "VALIDATION_ERROR"

        # Invalid email
        resp = session.post(f"{base_url}/register", json={
            "email": "invalid",
            "password": "testpass123",
            "slug": "testuser",
            "display_name": "Test"
        })
        assert resp.status_code == 400
        assert "email" in resp.json()["error"]["message"].lower()

        # Short password
        resp = session.post(f"{base_url}/register", json={
            "email": "test@example.com",
            "password": "short",
            "slug": "testuser",
            "display_name": "Test"
        })
        assert resp.status_code == 400
        assert "password" in resp.json()["error"]["message"].lower()

        # Invalid slug
        resp = session.post(f"{base_url}/register", json={
            "email": "test@example.com",
            "password": "testpass123",
            "slug": "A",  # Too short, wrong case
            "display_name": "Test"
        })
        assert resp.status_code == 400

    def test_login_validation(self, session, base_url):
        """Test login validation errors."""
        # Missing fields
        resp = session.post(f"{base_url}/login", json={})
        assert resp.status_code == 400
        data = resp.json()
        assert data["error"]["code"] == "VALIDATION_ERROR"

        # Invalid credentials
        resp = session.post(f"{base_url}/login", json={
            "email": "nonexistent@example.com",
            "password": "wrongpass"
        })
        assert resp.status_code == 401
        assert resp.json()["error"]["code"] == "UNAUTHORIZED"

    def test_protected_endpoints_require_auth(self, session, base_url):
        """Test that protected endpoints require authentication."""
        # No auth header
        resp = session.get(f"{base_url}/tokens")
        assert resp.status_code == 401
        assert resp.json()["error"]["code"] == "UNAUTHORIZED"

        resp = session.get(f"{base_url}/me/tools")
        assert resp.status_code == 401

        resp = session.post(f"{base_url}/tools", json={})
        assert resp.status_code == 401

        # Invalid token
        headers = {"Authorization": "Bearer invalid_token"}
        resp = session.get(f"{base_url}/tokens", headers=headers)
        assert resp.status_code == 401

    def test_full_auth_flow(self, session, base_url):
        """Test complete registration -> login -> token flow."""
        import uuid

        # Generate unique test user
        unique = uuid.uuid4().hex[:8]
        email = f"test_{unique}@example.com"
        slug = f"testuser{unique}"

        # Register
        resp = session.post(f"{base_url}/register", json={
            "email": email,
            "password": "testpass123",
            "slug": slug,
            "display_name": "Test User"
        })
        # May fail if user already exists from previous test run
        if resp.status_code == 201:
            data = resp.json()["data"]
            assert data["slug"] == slug
            assert data["email"] == email

        # Login
        resp = session.post(f"{base_url}/login", json={
            "email": email,
            "password": "testpass123"
        })
        assert resp.status_code == 200
        data = resp.json()["data"]
        assert "token" in data
        assert data["token"].startswith("reg_")
        token = data["token"]

        # Use token to access protected endpoint
        headers = {"Authorization": f"Bearer {token}"}
        resp = session.get(f"{base_url}/me/tools", headers=headers)
        assert resp.status_code == 200
        assert "data" in resp.json()

        # List tokens
        resp = session.get(f"{base_url}/tokens", headers=headers)
        assert resp.status_code == 200
        tokens = resp.json()["data"]
        assert len(tokens) >= 1

        # Create another token
        resp = session.post(f"{base_url}/tokens", headers=headers, json={
            "name": "CLI token"
        })
        assert resp.status_code == 201
        new_token = resp.json()["data"]
        assert new_token["name"] == "CLI token"
        assert "token" in new_token


@pytest.mark.integration
class TestPublishIntegration:
    """Integration tests for publishing tools.

    Run with: pytest tests/test_registry_integration.py -v -m integration
    """

    @pytest.fixture
    def base_url(self):
        return "http://localhost:5000/api/v1"

    @pytest.fixture
    def session(self):
        import requests
        return requests.Session()

    @pytest.fixture
    def auth_headers(self, session, base_url):
        """Get auth headers for a test user."""
        import uuid
        unique = uuid.uuid4().hex[:8]
        email = f"pub_{unique}@example.com"
        slug = f"publisher{unique}"

        # Register
        session.post(f"{base_url}/register", json={
            "email": email,
            "password": "testpass123",
            "slug": slug,
            "display_name": "Publisher"
        })

        # Login
        resp = session.post(f"{base_url}/login", json={
            "email": email,
            "password": "testpass123"
        })
        token = resp.json()["data"]["token"]
        return {"Authorization": f"Bearer {token}"}, slug

    def test_publish_validation(self, session, base_url, auth_headers):
        """Test publish validation errors."""
        headers, slug = auth_headers

        # Empty config
        resp = session.post(f"{base_url}/tools", headers=headers, json={
            "config": "",
            "readme": ""
        })
        assert resp.status_code == 400

        # Invalid YAML
        resp = session.post(f"{base_url}/tools", headers=headers, json={
            "config": "{{invalid yaml",
            "readme": ""
        })
        assert resp.status_code == 400
        assert resp.json()["error"]["code"] == "VALIDATION_ERROR"

        # Missing required fields
        resp = session.post(f"{base_url}/tools", headers=headers, json={
            "config": "description: no name or version",
            "readme": ""
        })
        assert resp.status_code == 400

        # Invalid version (not semver)
        resp = session.post(f"{base_url}/tools", headers=headers, json={
            "config": "name: test\nversion: bad",
            "readme": ""
        })
        assert resp.status_code == 400
        assert resp.json()["error"]["code"] == "INVALID_VERSION"

    def test_publish_dry_run(self, session, base_url, auth_headers):
        """Test publish dry run mode."""
        headers, slug = auth_headers
        import uuid
        tool_name = f"testtool{uuid.uuid4().hex[:8]}"

        config = f"""name: {tool_name}
version: 1.0.0
description: A test tool
category: text-processing
"""

        resp = session.post(f"{base_url}/tools", headers=headers, json={
            "config": config,
            "readme": "# Test Tool",
            "dry_run": True
        })
        assert resp.status_code == 200
        data = resp.json()["data"]
        assert data["status"] == "validated"
        assert data["name"] == tool_name
        assert data["owner"] == slug

    def test_publish_and_retrieve(self, session, base_url, auth_headers):
        """Test publishing a tool and retrieving it."""
        headers, slug = auth_headers
        import uuid
        tool_name = f"testtool{uuid.uuid4().hex[:8]}"

        config = f"""name: {tool_name}
version: 1.0.0
description: A test tool for integration testing
category: text-processing
tags:
  - test
  - integration
"""

        # Publish
        resp = session.post(f"{base_url}/tools", headers=headers, json={
            "config": config,
            "readme": "# Test Tool\n\nThis is a test."
        })
        assert resp.status_code == 201
        data = resp.json()["data"]
        assert data["owner"] == slug
        assert data["name"] == tool_name
        assert data["version"] == "1.0.0"

        # Retrieve
        resp = session.get(f"{base_url}/tools/{slug}/{tool_name}")
        assert resp.status_code == 200
        tool = resp.json()["data"]
        assert tool["name"] == tool_name
        assert tool["description"] == "A test tool for integration testing"
        assert "test" in tool["tags"]

        # Check my-tools includes it
        resp = session.get(f"{base_url}/me/tools", headers=headers)
        assert resp.status_code == 200
        my_tools = resp.json()["data"]
        assert any(t["name"] == tool_name for t in my_tools)

    def test_publish_duplicate_version(self, session, base_url, auth_headers):
        """Test that publishing duplicate version fails."""
        headers, slug = auth_headers
        import uuid
        tool_name = f"testtool{uuid.uuid4().hex[:8]}"

        config = f"""name: {tool_name}
version: 1.0.0
description: First version
"""

        # First publish
        resp = session.post(f"{base_url}/tools", headers=headers, json={
            "config": config,
            "readme": ""
        })
        assert resp.status_code == 201

        # Duplicate publish
        resp = session.post(f"{base_url}/tools", headers=headers, json={
            "config": config,
            "readme": ""
        })
        assert resp.status_code == 409
        assert resp.json()["error"]["code"] == "VERSION_EXISTS"


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
