"""
Unit and integration tests for OASist versioning functionality.
"""
import pytest
import sys
import json
import tempfile
import shutil
from pathlib import Path
from unittest.mock import patch, MagicMock, Mock
from datetime import datetime

# Add the package to Python path
sys.path.insert(0, str(Path(__file__).parent.parent))

from oasist.oasist import (
    VersioningConfig, VersionConfig, ServiceConfig,
    VersionDetector, VersionRegistry, ClientGenerator,
    ConfigLoader, SchemaProcessor, GenerateCommand, VersionsCommand,
    SchemaComparator, GeneratorRunner
)


# ============================================================================
# Unit Tests - VersioningConfig and VersionConfig
# ============================================================================
class TestVersioningConfig:
    """Unit tests for VersioningConfig dataclass."""
    
    def test_versioning_config_defaults(self):
        """Test VersioningConfig with default values."""
        config = VersioningConfig()
        assert config.enabled is False
        assert config.auto_detect is False
        assert config.base_version is None
    
    def test_versioning_config_custom(self):
        """Test VersioningConfig with custom values."""
        config = VersioningConfig(
            enabled=True,
            auto_detect=True,
            base_version="1.0.0"
        )
        assert config.enabled is True
        assert config.auto_detect is True
        assert config.base_version == "1.0.0"
    
    def test_versioning_config_generation_mode_default(self):
        """Test VersioningConfig with default generation_mode."""
        config = VersioningConfig()
        assert config.generation_mode == "full"
    
    def test_versioning_config_generation_mode_full(self):
        """Test VersioningConfig with generation_mode='full'."""
        config = VersioningConfig(generation_mode="full")
        assert config.generation_mode == "full"
    
    def test_versioning_config_generation_mode_changed(self):
        """Test VersioningConfig with generation_mode='changed'."""
        config = VersioningConfig(generation_mode="changed")
        assert config.generation_mode == "changed"
    
    def test_versioning_config_generation_mode_case_insensitive(self):
        """Test VersioningConfig normalizes generation_mode to lowercase."""
        config = VersioningConfig(generation_mode="CHANGED")
        assert config.generation_mode == "changed"
    
    def test_versioning_config_generation_mode_invalid(self):
        """Test VersioningConfig raises error for invalid generation_mode."""
        with pytest.raises(ValueError, match="generation_mode must be 'full' or 'changed'"):
            VersioningConfig(generation_mode="invalid")


class TestVersionConfig:
    """Unit tests for VersionConfig dataclass."""
    
    def test_version_config_basic(self):
        """Test VersionConfig with basic values."""
        config = VersionConfig(
            version="1.0.0",
            input={"target": "http://test.com/api"},
            output={"base_url": "http://test.com"}
        )
        assert config.version == "1.0.0"
        assert config.input["target"] == "http://test.com/api"
        assert config.output["base_url"] == "http://test.com"
    
    def test_version_config_env_substitution(self, monkeypatch):
        """Test environment variable substitution in VersionConfig."""
        monkeypatch.setenv("API_URL", "http://production.com")
        config = VersionConfig(
            version="1.0.0",
            input={"target": "${API_URL}/openapi.json"},
            output={"base_url": "${API_URL}"}
        )
        assert "production.com" in config.input["target"]
        assert "production.com" in config.output["base_url"]


class TestServiceConfigWithVersioning:
    """Unit tests for ServiceConfig with versioning support."""
    
    def test_service_config_with_versioning(self):
        """Test ServiceConfig with versioning enabled."""
        versioning = VersioningConfig(enabled=True, auto_detect=True)
        versions = {
            "1.0.0": VersionConfig(
                version="1.0.0",
                input={"target": "http://test.com/v1/openapi.json"},
                output={"base_url": "http://test.com"}
            )
        }
        config = ServiceConfig(
            name="Test API",
            schema_url="http://test.com/openapi.json",
            output_dir="test",
            versioning=versioning,
            versions=versions
        )
        assert config.versioning is not None
        assert config.versioning.enabled is True
        assert len(config.versions) == 1
        assert "1.0.0" in config.versions
    
    def test_service_config_without_versioning(self):
        """Test ServiceConfig without versioning (backward compatible)."""
        config = ServiceConfig(
            name="Test API",
            schema_url="http://test.com/openapi.json",
            output_dir="test"
        )
        assert config.versioning is None
        assert len(config.versions) == 0


# ============================================================================
# Unit Tests - VersionDetector
# ============================================================================
class TestVersionDetector:
    """Unit tests for VersionDetector class."""
    
    def test_detect_from_schema_with_version(self):
        """Test detecting version from OpenAPI schema."""
        schema = {
            "openapi": "3.0.0",
            "info": {
                "title": "Test API",
                "version": "1.8.1"
            }
        }
        version = VersionDetector.detect_from_schema(schema)
        assert version == "1.8.1"
    
    def test_detect_from_schema_with_v_prefix(self):
        """Test detecting version with v prefix."""
        schema = {
            "openapi": "3.0.0",
            "info": {
                "title": "Test API",
                "version": "v1.0.0"
            }
        }
        version = VersionDetector.detect_from_schema(schema)
        assert version == "v1.0.0"  # Should preserve exact string
    
    def test_detect_from_schema_no_version(self):
        """Test detecting version when not present."""
        schema = {
            "openapi": "3.0.0",
            "info": {
                "title": "Test API"
            }
        }
        version = VersionDetector.detect_from_schema(schema)
        assert version is None
    
    def test_detect_from_schema_no_info(self):
        """Test detecting version when info section is missing."""
        schema = {
            "openapi": "3.0.0",
            "paths": {}
        }
        version = VersionDetector.detect_from_schema(schema)
        assert version is None
    
    def test_normalize_for_directory(self):
        """Test normalizing version for directory name."""
        assert VersionDetector.normalize_for_directory("1.0.0") == "1.0.0"
        assert VersionDetector.normalize_for_directory("v1.0.0") == "v1.0.0"
        assert VersionDetector.normalize_for_directory("1.0.0-beta") == "1.0.0-beta"
        # Test with invalid filesystem characters
        normalized = VersionDetector.normalize_for_directory("1.0.0/test")
        assert "/" not in normalized
    
    def test_normalize_for_import(self):
        """Test normalizing version for Python import."""
        assert VersionDetector.normalize_for_import("1.0.0") == "v1_0_0"  # Starts with number, dots replaced
        assert VersionDetector.normalize_for_import("v1.0.0") == "v1_0_0"
        assert VersionDetector.normalize_for_import("1.8.1") == "v1_8_1"
        assert VersionDetector.normalize_for_import("1.1.0") == "v1_1_0"


# ============================================================================
# Unit Tests - VersionRegistry
# ============================================================================
class TestVersionRegistry:
    """Unit tests for VersionRegistry class."""
    
    def test_load_nonexistent_registry(self, tmp_path):
        """Test loading nonexistent registry file."""
        registry_path = tmp_path / "version_registry.json"
        data = VersionRegistry.load(registry_path)
        assert data["versions"] == {}
        assert data["metadata"]["base_version"] is None
        assert data["metadata"]["latest_version"] is None
    
    def test_save_and_load_registry(self, tmp_path):
        """Test saving and loading registry."""
        registry_path = tmp_path / "version_registry.json"
        data = {
            "versions": {
                "1.0.0": {
                    "generated_at": "2024-01-01T00:00:00Z",
                    "openapi_version": "3.0.0"
                }
            },
            "metadata": {
                "base_version": "1.0.0",
                "latest_version": "1.0.0"
            }
        }
        VersionRegistry.save(registry_path, data)
        
        loaded = VersionRegistry.load(registry_path)
        assert loaded["versions"]["1.0.0"]["openapi_version"] == "3.0.0"
        assert loaded["metadata"]["base_version"] == "1.0.0"
    
    def test_add_version(self, tmp_path):
        """Test adding version to registry."""
        registry_path = tmp_path / "version_registry.json"
        metadata = {
            "openapi_version": "3.0.0",
            "schema_url": "http://test.com/openapi.json",
            "endpoints": ["GET /users", "POST /users"],
            "endpoint_count": 2
        }
        VersionRegistry.add_version(registry_path, "1.0.0", metadata)
        
        data = VersionRegistry.load(registry_path)
        assert "1.0.0" in data["versions"]
        assert data["versions"]["1.0.0"]["openapi_version"] == "3.0.0"
        assert data["metadata"]["base_version"] == "1.0.0"
        assert data["metadata"]["latest_version"] == "1.0.0"
    
    def test_add_multiple_versions(self, tmp_path):
        """Test adding multiple versions."""
        registry_path = tmp_path / "version_registry.json"
        VersionRegistry.add_version(registry_path, "1.0.0", {"openapi_version": "3.0.0"})
        VersionRegistry.add_version(registry_path, "1.1.0", {"openapi_version": "3.0.0"})
        
        data = VersionRegistry.load(registry_path)
        assert len(data["versions"]) == 2
        assert data["metadata"]["latest_version"] == "1.1.0"
    
    def test_get_versions(self, tmp_path):
        """Test getting all versions."""
        registry_path = tmp_path / "version_registry.json"
        VersionRegistry.add_version(registry_path, "1.0.0", {"openapi_version": "3.0.0"})
        VersionRegistry.add_version(registry_path, "1.1.0", {"openapi_version": "3.0.0"})
        
        versions = VersionRegistry.get_versions(registry_path)
        assert "1.0.0" in versions
        assert "1.1.0" in versions
        assert len(versions) == 2
    
    def test_get_latest_version(self, tmp_path):
        """Test getting latest version."""
        registry_path = tmp_path / "version_registry.json"
        VersionRegistry.add_version(registry_path, "1.0.0", {"openapi_version": "3.0.0"})
        VersionRegistry.add_version(registry_path, "1.1.0", {"openapi_version": "3.0.0"})
        
        latest = VersionRegistry.get_latest_version(registry_path)
        assert latest == "1.1.0"
    
    def test_get_latest_version_empty(self, tmp_path):
        """Test getting latest version from empty registry."""
        registry_path = tmp_path / "version_registry.json"
        latest = VersionRegistry.get_latest_version(registry_path)
        assert latest is None
    
    def test_get_base_version(self, tmp_path):
        """Test getting base version from registry."""
        registry_path = tmp_path / "version_registry.json"
        VersionRegistry.add_version(registry_path, "1.0.0", {"openapi_version": "3.0.0"})
        VersionRegistry.add_version(registry_path, "1.1.0", {"openapi_version": "3.0.0"})
        
        base_version = VersionRegistry.get_base_version(registry_path)
        assert base_version == "1.0.0"
    
    def test_add_version_with_changed_metadata(self, tmp_path):
        """Test adding version with changed endpoints/models metadata."""
        registry_path = tmp_path / "version_registry.json"
        metadata = {
            "openapi_version": "3.0.0",
            "schema_url": "http://test.com/openapi.json",
            "endpoints": ["GET /users", "POST /users"],
            "endpoint_count": 2,
            "models": ["User", "UserCreate"],
            "changed_endpoints": ["POST /users"],
            "changed_models": ["UserCreate"],
            "base_version": "1.0.0"
        }
        VersionRegistry.add_version(registry_path, "1.1.0", metadata)
        
        data = VersionRegistry.load(registry_path)
        assert "1.1.0" in data["versions"]
        assert data["versions"]["1.1.0"]["changed_endpoints"] == ["POST /users"]
        assert data["versions"]["1.1.0"]["changed_models"] == ["UserCreate"]
        assert data["versions"]["1.1.0"]["base_version"] == "1.0.0"
    
    def test_load_version_schema_from_file(self, tmp_path):
        """Test loading version schema from cached file."""
        # Create a test schema file
        schema_file = tmp_path / "test_schema.json"
        schema_data = {
            "openapi": "3.0.0",
            "info": {"title": "Test", "version": "1.0.0"},
            "paths": {}
        }
        schema_file.write_text(json.dumps(schema_data))
        
        # Add version with file path
        registry_path = tmp_path / "version_registry.json"
        VersionRegistry.add_version(registry_path, "1.0.0", {
            "schema_url": str(schema_file),
            "openapi_version": "3.0.0"
        })
        
        # Load schema
        loaded_schema = VersionRegistry.load_version_schema(registry_path, "1.0.0")
        assert loaded_schema is not None
        assert loaded_schema["info"]["version"] == "1.0.0"
    
    def test_get_base_schema_path(self, tmp_path):
        """Test getting base schema path from registry."""
        schema_file = tmp_path / "base_schema.json"
        schema_file.write_text(json.dumps({"openapi": "3.0.0", "info": {"version": "1.0.0"}}))
        
        registry_path = tmp_path / "version_registry.json"
        VersionRegistry.add_version(registry_path, "1.0.0", {
            "schema_url": str(schema_file),
            "openapi_version": "3.0.0"
        })
        VersionRegistry.add_version(registry_path, "1.1.0", {
            "schema_url": "http://test.com/openapi.json",
            "openapi_version": "3.0.0",
            "base_version": "1.0.0"
        })
        
        base_path = VersionRegistry.get_base_schema_path(registry_path, "1.1.0")
        assert base_path is not None
        assert base_path == schema_file.resolve()


# ============================================================================
# Unit Tests - SchemaComparator
# ============================================================================
class TestSchemaComparator:
    """Unit tests for SchemaComparator class."""
    
    def test_get_endpoint_signature(self):
        """Test creating endpoint signature."""
        sig = SchemaComparator.get_endpoint_signature("/api/users", "get")
        assert sig == "GET /api/users"
        
        sig = SchemaComparator.get_endpoint_signature("/api/user/{id}", "post")
        assert sig == "POST /api/user/{id}"
    
    def test_normalize_schema(self):
        """Test schema normalization."""
        schema = {
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"}
            },
            "description": "Test schema"
        }
        normalized = SchemaComparator.normalize_schema(schema)
        
        # Description should be removed
        assert "description" not in normalized
        # Properties should be preserved
        assert "properties" in normalized
        assert "name" in normalized["properties"]
    
    def test_compare_endpoints_new_endpoint(self):
        """Test comparing endpoints with new endpoint."""
        base_schema = {
            "paths": {
                "/users": {
                    "get": {"summary": "Get users"}
                }
            }
        }
        new_schema = {
            "paths": {
                "/users": {
                    "get": {"summary": "Get users"}
                },
                "/posts": {
                    "get": {"summary": "Get posts"}
                }
            }
        }
        
        result = SchemaComparator.compare_endpoints(base_schema, new_schema)
        assert "GET /posts" in result["new"]
        assert "GET /users" in result["unchanged"]
        assert len(result["new"]) == 1
        assert len(result["unchanged"]) == 1
    
    def test_compare_endpoints_modified_endpoint(self):
        """Test comparing endpoints with modified endpoint."""
        base_schema = {
            "paths": {
                "/users": {
                    "get": {
                        "summary": "Get users",
                        "responses": {
                            "200": {
                                "description": "Success",
                                "content": {
                                    "application/json": {
                                        "schema": {"type": "object"}
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        new_schema = {
            "paths": {
                "/users": {
                    "get": {
                        "summary": "Get users",
                        "responses": {
                            "200": {
                                "description": "Updated",
                                "content": {
                                    "application/json": {
                                        "schema": {"type": "array", "items": {"type": "object"}}
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        
        result = SchemaComparator.compare_endpoints(base_schema, new_schema)
        assert "GET /users" in result["modified"]
        assert len(result["modified"]) == 1
    
    def test_compare_endpoints_removed_endpoint(self):
        """Test comparing endpoints with removed endpoint."""
        base_schema = {
            "paths": {
                "/users": {"get": {}},
                "/posts": {"get": {}}
            }
        }
        new_schema = {
            "paths": {
                "/users": {"get": {}}
            }
        }
        
        result = SchemaComparator.compare_endpoints(base_schema, new_schema)
        assert "GET /posts" in result["removed"]
        assert len(result["removed"]) == 1
    
    def test_compare_models_new_model(self):
        """Test comparing models with new model."""
        base_schema = {
            "components": {
                "schemas": {
                    "User": {
                        "type": "object",
                        "properties": {"name": {"type": "string"}}
                    }
                }
            }
        }
        new_schema = {
            "components": {
                "schemas": {
                    "User": {
                        "type": "object",
                        "properties": {"name": {"type": "string"}}
                    },
                    "Post": {
                        "type": "object",
                        "properties": {"title": {"type": "string"}}
                    }
                }
            }
        }
        
        result = SchemaComparator.compare_models(base_schema, new_schema)
        assert "Post" in result["new"]
        assert "User" in result["unchanged"]
        assert len(result["new"]) == 1
        assert len(result["unchanged"]) == 1
    
    def test_compare_models_modified_model(self):
        """Test comparing models with modified model."""
        base_schema = {
            "components": {
                "schemas": {
                    "User": {
                        "type": "object",
                        "properties": {
                            "name": {"type": "string"}
                        }
                    }
                }
            }
        }
        new_schema = {
            "components": {
                "schemas": {
                    "User": {
                        "type": "object",
                        "properties": {
                            "name": {"type": "string"},
                            "email": {"type": "string"}
                        }
                    }
                }
            }
        }
        
        result = SchemaComparator.compare_models(base_schema, new_schema)
        assert "User" in result["modified"]
        assert len(result["modified"]) == 1
    
    def test_compare_models_unchanged_model(self):
        """Test comparing models with unchanged model."""
        schema = {
            "components": {
                "schemas": {
                    "User": {
                        "type": "object",
                        "properties": {"name": {"type": "string"}}
                    }
                }
            }
        }
        
        result = SchemaComparator.compare_models(schema, schema)
        assert "User" in result["unchanged"]
        assert len(result["unchanged"]) == 1
        assert len(result["new"]) == 0
        assert len(result["modified"]) == 0
    
    def test_compare_operation_identical(self):
        """Test comparing identical operations."""
        op1 = {
            "summary": "Get users",
            "responses": {"200": {"description": "Success"}}
        }
        op2 = {
            "summary": "Get users",
            "responses": {"200": {"description": "Success"}}
        }
        
        assert SchemaComparator.compare_operation(op1, op2) is True
    
    def test_compare_operation_different(self):
        """Test comparing different operations."""
        op1 = {
            "summary": "Get users",
            "responses": {
                "200": {
                    "description": "Success",
                    "content": {
                        "application/json": {
                            "schema": {"type": "object"}
                        }
                    }
                }
            }
        }
        op2 = {
            "summary": "Get users",
            "responses": {
                "200": {
                    "description": "Updated",
                    "content": {
                        "application/json": {
                            "schema": {"type": "array"}
                        }
                    }
                }
            }
        }
        
        assert SchemaComparator.compare_operation(op1, op2) is False


# ============================================================================
# Unit Tests - ConfigLoader with Versioning
# ============================================================================
class TestConfigLoaderVersioning:
    """Unit tests for ConfigLoader with versioning support."""
    
    def test_load_versioned_project(self, tmp_path):
        """Test loading project with versioning enabled."""
        config_data = {
            "output_dir": str(tmp_path),
            "projects": {
                "test_api": {
                    "versioning": {
                        "enabled": True,
                        "auto_detect": True,
                        "base_version": "1.0.0"
                    },
                    "versions": {
                        "1.0.0": {
                            "input": {"target": "http://test.com/v1/openapi.json"},
                            "output": {"base_url": "http://test.com"}
                        },
                        "1.1.0": {
                            "input": {"target": "http://test.com/v1.1/openapi.json"},
                            "output": {"base_url": "http://test.com"}
                        }
                    }
                }
            }
        }
        config_file = tmp_path / "config.json"
        config_file.write_text(json.dumps(config_data))
        
        generator = ClientGenerator()
        result = ConfigLoader.load(generator, str(config_file))
        
        assert result is True
        assert "test_api" in generator.services
        config = generator.services["test_api"]
        assert config.versioning is not None
        assert config.versioning.enabled is True
        assert len(config.versions) == 2
        assert "1.0.0" in config.versions
        assert "1.1.0" in config.versions
    
    def test_load_non_versioned_project(self, tmp_path):
        """Test loading non-versioned project (backward compatible)."""
        config_data = {
            "output_dir": str(tmp_path),
            "projects": {
                "test_api": {
                    "input": {"target": "http://test.com/openapi.json"},
                    "output": {"dir": "test_api", "name": "Test API"}
                }
            }
        }
        config_file = tmp_path / "config.json"
        config_file.write_text(json.dumps(config_data))
        
        generator = ClientGenerator()
        result = ConfigLoader.load(generator, str(config_file))
        
        assert result is True
        config = generator.services["test_api"]
        assert config.versioning is None or config.versioning.enabled is False
    
    def test_load_versioned_project_with_generation_mode(self, tmp_path):
        """Test loading project with generation_mode configuration."""
        config_data = {
            "output_dir": str(tmp_path),
            "projects": {
                "test_api": {
                    "versioning": {
                        "enabled": True,
                        "auto_detect": False,
                        "base_version": "1.0.0",
                        "generation_mode": "changed"
                    },
                    "versions": {
                        "1.0.0": {
                            "input": {"target": "http://test.com/v1/openapi.json"},
                            "output": {"base_url": "http://test.com"}
                        }
                    }
                }
            }
        }
        config_file = tmp_path / "config.json"
        config_file.write_text(json.dumps(config_data))
        
        generator = ClientGenerator()
        result = ConfigLoader.load(generator, str(config_file))
        
        assert result is True
        config = generator.services["test_api"]
        # Generation mode should be preserved as "changed" in config
        # Base version will be forced to full mode during generation, not in config loading
        assert config.versioning.generation_mode == "changed"
    
    def test_load_versioned_project_with_invalid_generation_mode(self, tmp_path):
        """Test loading project with invalid generation_mode falls back to full."""
        config_data = {
            "output_dir": str(tmp_path),
            "projects": {
                "test_api": {
                    "versioning": {
                        "enabled": True,
                        "generation_mode": "invalid_mode"
                    },
                    "versions": {
                        "1.0.0": {
                            "input": {"target": "http://test.com/v1/openapi.json"},
                            "output": {"base_url": "http://test.com"}
                        }
                    }
                }
            }
        }
        config_file = tmp_path / "config.json"
        config_file.write_text(json.dumps(config_data))
        
        generator = ClientGenerator()
        result = ConfigLoader.load(generator, str(config_file))
        
        assert result is True
        config = generator.services["test_api"]
        # Should fallback to "full" mode
        assert config.versioning.generation_mode == "full"


# ============================================================================
# Integration Tests - Version Detection
# ============================================================================
class TestVersionDetectionIntegration:
    """Integration tests for version detection from real APIs."""
    
    @pytest.mark.integration
    def test_detect_version_from_pasarguard_1_8_1(self):
        """Test detecting version from PasarGuardAPI 1.8.1."""
        try:
            schema = SchemaProcessor.fetch(
                "https://sub.giggo.site/openapi.json",
                params={},
                prefer_json=True
            )
            
            if schema is None:
                pytest.skip("API not available or returned None")
            
            version = VersionDetector.detect_from_schema(schema)
            assert version == "1.8.1", f"Expected version 1.8.1, got {version}"
            assert schema.get("info", {}).get("title") == "PasarGuardAPI"
        except Exception as e:
            pytest.skip(f"API not available: {e}")
    
    @pytest.mark.integration
    @pytest.mark.skipif(
        True,  # Skip by default - requires local server
        reason="Requires local server at localhost:8000"
    )
    def test_detect_version_from_pasarguard_1_0_0(self):
        """Test detecting version from PasarGuardAPI 1.0.0."""
        schema = SchemaProcessor.fetch(
            "http://localhost:8000/openapi.json",
            params={},
            prefer_json=True
        )
        
        if schema is None:
            pytest.skip("API not available at localhost:8000")
        
        version = VersionDetector.detect_from_schema(schema)
        assert version == "1.0.0"
    
    @pytest.mark.integration
    @pytest.mark.skipif(
        True,  # Skip by default - requires local server
        reason="Requires local server at localhost:8001"
    )
    def test_detect_version_from_pasarguard_1_1_0(self):
        """Test detecting version from PasarGuardAPI 1.1.0."""
        schema = SchemaProcessor.fetch(
            "http://localhost:8001/openapi.json",
            params={},
            prefer_json=True
        )
        
        if schema is None:
            pytest.skip("API not available at localhost:8001")
        
        version = VersionDetector.detect_from_schema(schema)
        assert version == "1.1.0"


# ============================================================================
# Integration Tests - Versioned Client Generation
# ============================================================================
class TestVersionedClientGeneration:
    """Integration tests for generating versioned clients."""
    
    @pytest.mark.integration
    @patch.object(ClientGenerator, '_generate_versioned_client', return_value=True)
    @patch.object(ClientGenerator, '_generate_versioned_entry_point')
    @patch.object(SchemaProcessor, 'fetch')
    def test_generate_versioned_client(self, mock_fetch, mock_entry, mock_gen, tmp_path):
        """Test generating versioned client."""
        # Mock schema fetch
        mock_fetch.return_value = {
            "openapi": "3.0.0",
            "info": {"title": "Test API", "version": "1.0.0"},
            "paths": {
                "/users": {"get": {}}
            }
        }
        
        generator = ClientGenerator(output_base=tmp_path)
        versioning = VersioningConfig(enabled=True, auto_detect=False)
        versions = {
            "1.0.0": VersionConfig(
                version="1.0.0",
                input={"target": "http://test.com/v1/openapi.json"},
                output={"base_url": "http://test.com", "format_with_black": False}
            )
        }
        config = ServiceConfig(
            name="Test API",
            schema_url="http://test.com/openapi.json",
            output_dir="test_api",
            versioning=versioning,
            versions=versions
        )
        generator.add_service("test_api", config)
        
        result = generator.generate("test_api", force=True)
        assert result is True
        assert mock_gen.called
        assert mock_entry.called
    
    @pytest.mark.integration
    @patch.object(ClientGenerator, '_generate_versioned_client', return_value=True)
    @patch.object(ClientGenerator, '_generate_versioned_entry_point')
    @patch.object(SchemaProcessor, 'fetch')
    def test_generate_multiple_versions(self, mock_fetch, mock_entry, mock_gen, tmp_path):
        """Test generating multiple versions."""
        mock_fetch.return_value = {
            "openapi": "3.0.0",
            "info": {"title": "Test API", "version": "1.0.0"},
            "paths": {"/users": {"get": {}}}
        }
        
        generator = ClientGenerator(output_base=tmp_path)
        versioning = VersioningConfig(enabled=True, auto_detect=False)
        versions = {
            "1.0.0": VersionConfig(
                version="1.0.0",
                input={"target": "http://test.com/v1/openapi.json"},
                output={"base_url": "http://test.com", "format_with_black": False}
            ),
            "1.1.0": VersionConfig(
                version="1.1.0",
                input={"target": "http://test.com/v1.1/openapi.json"},
                output={"base_url": "http://test.com", "format_with_black": False}
            )
        }
        config = ServiceConfig(
            name="Test API",
            schema_url="http://test.com/openapi.json",
            output_dir="test_api",
            versioning=versioning,
            versions=versions
        )
        generator.add_service("test_api", config)
        
        result = generator.generate("test_api", force=True)
        assert result is True
        # Should generate both versions
        assert mock_gen.call_count == 2
    
    @pytest.mark.integration
    def test_generate_specific_version(self, tmp_path):
        """Test generating specific version."""
        generator = ClientGenerator(output_base=tmp_path)
        versioning = VersioningConfig(enabled=True, auto_detect=False)
        versions = {
            "1.0.0": VersionConfig(
                version="1.0.0",
                input={"target": "http://test.com/v1/openapi.json"},
                output={"base_url": "http://test.com", "format_with_black": False}
            ),
            "1.1.0": VersionConfig(
                version="1.1.0",
                input={"target": "http://test.com/v1.1/openapi.json"},
                output={"base_url": "http://test.com", "format_with_black": False}
            )
        }
        config = ServiceConfig(
            name="Test API",
            schema_url="http://test.com/openapi.json",
            output_dir="test_api",
            versioning=versioning,
            versions=versions
        )
        generator.add_service("test_api", config)
        
        # Generate only version 1.1.0
        with patch.object(ClientGenerator, '_generate_versioned_client', return_value=True) as mock_gen, \
             patch.object(ClientGenerator, '_generate_versioned_entry_point'), \
             patch.object(SchemaProcessor, 'fetch') as mock_fetch:
            mock_fetch.return_value = {
                "openapi": "3.0.0",
                "info": {"title": "Test API", "version": "1.1.0"},
                "paths": {"/users": {"get": {}}}
            }
            
            result = generator.generate("test_api", force=True, version="1.1.0")
            assert result is True
            assert mock_gen.call_count == 1


# ============================================================================
# Integration Tests - Full Workflow with Real APIs
# ============================================================================
class TestFullVersioningWorkflow:
    """Full integration tests with real API endpoints."""
    
    @pytest.mark.integration
    def test_full_workflow_pasarguard_1_8_1(self, tmp_path):
        """Test full workflow with PasarGuardAPI 1.8.1."""
        config_data = {
            "output_dir": str(tmp_path),
            "projects": {
                "pasarguard": {
                    "versioning": {
                        "enabled": True,
                        "auto_detect": True,
                        "base_version": "1.8.1"
                    },
                    "versions": {
                        "1.8.1": {
                            "input": {
                                "target": "https://sub.giggo.site/openapi.json",
                                "prefer_json": True
                            },
                            "output": {
                                "base_url": "https://sub.giggo.site",
                                "package_name": "pasarguard",
                                "format_with_black": False
                            }
                        }
                    }
                }
            }
        }
        config_file = tmp_path / "config.json"
        config_file.write_text(json.dumps(config_data))
        
        generator = ClientGenerator()
        result = ConfigLoader.load(generator, str(config_file))
        assert result is True
        
        config = generator.services["pasarguard"]
        assert config.versioning.enabled is True
        assert "1.8.1" in config.versions
        
        # Test version detection
        schema = SchemaProcessor.fetch(
            "https://sub.giggo.site/openapi.json",
            params={},
            prefer_json=True
        )
        if schema:
            detected_version = VersionDetector.detect_from_schema(schema)
            assert detected_version == "1.8.1"
    
    @pytest.mark.integration
    @pytest.mark.skipif(
        True,
        reason="Requires local servers"
    )
    def test_full_workflow_multiple_versions(self, tmp_path):
        """Test full workflow with multiple PasarGuardAPI versions."""
        config_data = {
            "output_dir": str(tmp_path),
            "projects": {
                "pasarguard": {
                    "versioning": {
                        "enabled": True,
                        "auto_detect": True,
                        "base_version": "1.0.0"
                    },
                    "versions": {
                        "1.0.0": {
                            "input": {
                                "target": "http://localhost:8000/openapi.json",
                                "prefer_json": True
                            },
                            "output": {
                                "base_url": "http://localhost:8000",
                                "package_name": "pasarguard",
                                "format_with_black": False
                            }
                        },
                        "1.1.0": {
                            "input": {
                                "target": "http://localhost:8001/openapi.json",
                                "prefer_json": True
                            },
                            "output": {
                                "base_url": "http://localhost:8001",
                                "package_name": "pasarguard",
                                "format_with_black": False
                            }
                        }
                    }
                }
            }
        }
        config_file = tmp_path / "config.json"
        config_file.write_text(json.dumps(config_data))
        
        generator = ClientGenerator()
        result = ConfigLoader.load(generator, str(config_file))
        assert result is True
        
        config = generator.services["pasarguard"]
        assert len(config.versions) == 2
        
        # Test version detection for both
        schema_1_0 = SchemaProcessor.fetch(
            "http://localhost:8000/openapi.json",
            params={},
            prefer_json=True
        )
        if schema_1_0:
            version_1_0 = VersionDetector.detect_from_schema(schema_1_0)
            assert version_1_0 == "1.0.0"
        
        schema_1_1 = SchemaProcessor.fetch(
            "http://localhost:8001/openapi.json",
            params={},
            prefer_json=True
        )
        if schema_1_1:
            version_1_1 = VersionDetector.detect_from_schema(schema_1_1)
            assert version_1_1 == "1.1.0"


# ============================================================================
# Integration Tests - CLI Commands with Versioning
# ============================================================================
class TestCLIVersioning:
    """Integration tests for CLI commands with versioning."""
    
    def test_generate_command_with_version_flag(self):
        """Test GenerateCommand with --version flag."""
        generator = Mock()
        cmd = GenerateCommand()
        cmd.execute(generator, ["generate", "test_api", "--version", "1.1.0"])
        generator.generate.assert_called_once_with("test_api", False, "1.1.0")
    
    def test_generate_command_with_version_and_force(self):
        """Test GenerateCommand with --version and --force flags."""
        generator = Mock()
        cmd = GenerateCommand()
        cmd.execute(generator, ["generate", "test_api", "--version", "1.1.0", "--force"])
        generator.generate.assert_called_once_with("test_api", True, "1.1.0")
    
    def test_versions_command(self):
        """Test VersionsCommand execution."""
        generator = Mock()
        cmd = VersionsCommand()
        cmd.execute(generator, ["versions", "test_api"])
        generator.list_versions.assert_called_once_with("test_api")


# ============================================================================
# Unit Tests - Incremental Generation Logic
# ============================================================================
class TestIncrementalGeneration:
    """Unit tests for incremental generation functionality."""
    
    def test_filter_schema_for_changes(self, tmp_path):
        """Test filtering schema to include only changed endpoints/models."""
        generator = ClientGenerator(output_base=tmp_path)
        
        base_schema = {
            "openapi": "3.0.0",
            "info": {"title": "Test API", "version": "1.0.0"},
            "paths": {
                "/users": {"get": {"summary": "Get users"}},
                "/posts": {"get": {"summary": "Get posts"}}
            },
            "components": {
                "schemas": {
                    "User": {"type": "object", "properties": {"name": {"type": "string"}}},
                    "Post": {"type": "object", "properties": {"title": {"type": "string"}}}
                }
            }
        }
        
        new_schema = {
            "openapi": "3.0.0",
            "info": {"title": "Test API", "version": "1.1.0"},
            "paths": {
                "/users": {"get": {"summary": "Get users"}},
                "/posts": {"get": {"summary": "Get posts"}},
                "/comments": {"get": {"summary": "Get comments"}}
            },
            "components": {
                "schemas": {
                    "User": {"type": "object", "properties": {"name": {"type": "string"}}},
                    "Post": {"type": "object", "properties": {"title": {"type": "string"}}},
                    "Comment": {"type": "object", "properties": {"text": {"type": "string"}}}
                }
            }
        }
        
        endpoint_changes = SchemaComparator.compare_endpoints(base_schema, new_schema)
        model_changes = SchemaComparator.compare_models(base_schema, new_schema)
        
        filtered = generator._filter_schema_for_changes(new_schema, endpoint_changes, model_changes, base_schema)
        
        # Should only include new endpoint
        assert "/comments" in filtered["paths"]
        assert "/users" not in filtered["paths"]  # Unchanged
        assert "/posts" not in filtered["paths"]  # Unchanged
        
        # Should include new model
        assert "Comment" in filtered["components"]["schemas"]
    
    def test_extract_schema_refs(self, tmp_path):
        """Test extracting schema references from schema object."""
        generator = ClientGenerator(output_base=tmp_path)
        
        schema = {
            "$ref": "#/components/schemas/User"
        }
        refs = generator._extract_schema_refs(schema)
        assert "User" in refs
        
        schema = {
            "type": "array",
            "items": {
                "$ref": "#/components/schemas/Post"
            }
        }
        refs = generator._extract_schema_refs(schema)
        assert "Post" in refs
        
        schema = {
            "type": "object",
            "properties": {
                "user": {
                    "$ref": "#/components/schemas/User"
                },
                "post": {
                    "$ref": "#/components/schemas/Post"
                }
            }
        }
        refs = generator._extract_schema_refs(schema)
        assert "User" in refs
        assert "Post" in refs
    
    def test_generate_import_stubs(self, tmp_path):
        """Test generating import stubs for unchanged endpoints/models."""
        generator = ClientGenerator(output_base=tmp_path)
        
        # Create mock directory structure
        output_path = tmp_path / "1.1.0"
        api_dir = output_path / "api"
        models_dir = output_path / "models"
        api_dir.mkdir(parents=True)
        models_dir.mkdir(parents=True)
        
        # Create a module directory
        user_module = api_dir / "user"
        user_module.mkdir()
        
        endpoint_changes = {
            "new": ["GET /api/comments"],
            "modified": [],
            "unchanged": ["GET /api/user", "POST /api/user"],
            "removed": []
        }
        
        model_changes = {
            "new": ["Comment"],
            "modified": [],
            "unchanged": ["User", "UserCreate"]
        }
        
        generator._generate_import_stubs(output_path, "1.0.0", endpoint_changes, model_changes)
        
        # Check that import stub was created
        init_file = user_module / "__init__.py"
        assert init_file.exists()
        content = init_file.read_text()
        assert "from ...v1_0_0.api.user import *" in content
        
        # Check models import
        models_init = models_dir / "__init__.py"
        assert models_init.exists()
        content = models_init.read_text()
        assert "from ...v1_0_0.models import" in content


# ============================================================================
# Integration Tests - Incremental Generation
# ============================================================================
class TestIncrementalGenerationIntegration:
    """Integration tests for incremental generation."""
    
    @pytest.mark.integration
    @patch.object(GeneratorRunner, 'run', return_value=True)
    def test_incremental_generation_with_changes(self, mock_run, tmp_path):
        """Test incremental generation when changes are detected."""
        generator = ClientGenerator(output_base=tmp_path)
        
        base_schema = {
            "openapi": "3.0.0",
            "info": {"title": "Test API", "version": "1.0.0"},
            "paths": {
                "/api/users": {
                    "get": {
                        "summary": "Get users",
                        "responses": {"200": {"description": "Success"}}
                    }
                }
            },
            "components": {
                "schemas": {
                    "User": {"type": "object", "properties": {"name": {"type": "string"}}}
                }
            }
        }
        
        new_schema = {
            "openapi": "3.0.0",
            "info": {"title": "Test API", "version": "1.1.0"},
            "paths": {
                "/api/users": {
                    "get": {
                        "summary": "Get users",
                        "responses": {"200": {"description": "Success"}}
                    }
                },
                "/api/posts": {
                    "get": {
                        "summary": "Get posts",
                        "responses": {"200": {"description": "Success"}}
                    }
                }
            },
            "components": {
                "schemas": {
                    "User": {"type": "object", "properties": {"name": {"type": "string"}}},
                    "Post": {"type": "object", "properties": {"title": {"type": "string"}}}
                }
            }
        }
        
        # Create base version in registry
        registry_path = tmp_path / "test_api" / "version_registry.json"
        registry_path.parent.mkdir(parents=True)
        VersionRegistry.add_version(registry_path, "1.0.0", {
            "openapi_version": "3.0.0",
            "schema_url": "http://test.com/v1/openapi.json",
            "endpoints": ["GET /api/users"],
            "endpoint_count": 1
        })
        
        # Save base schema to file for loading
        base_schema_file = tmp_path / "base_schema.json"
        base_schema_file.write_text(json.dumps(base_schema))
        
        # Update registry with file path
        VersionRegistry.add_version(registry_path, "1.0.0", {
            "openapi_version": "3.0.0",
            "schema_url": str(base_schema_file),
            "endpoints": ["GET /api/users"],
            "endpoint_count": 1
        })
        
        versioning = VersioningConfig(
            enabled=True,
            auto_detect=False,
            base_version="1.0.0",
            generation_mode="changed"
        )
        
        version_config = VersionConfig(
            version="1.1.0",
            input={"target": "http://test.com/v1.1/openapi.json", "prefer_json": True},
            output={"base_url": "http://test.com", "format_with_black": False}
        )
        
        config = ServiceConfig(
            name="Test API",
            schema_url="http://test.com/openapi.json",
            output_dir="test_api",
            versioning=versioning,
            versions={"1.0.0": version_config, "1.1.0": version_config}
        )
        
        output_path = tmp_path / "test_api" / "1.1.0"
        
        with patch.object(SchemaProcessor, 'fetch', side_effect=[new_schema, base_schema]):
            result = generator._generate_versioned_client(
                "test_api", "1.1.0", version_config, output_path, True, config, registry_path
            )
        
        assert result is True
        assert mock_run.called
    
    @pytest.mark.integration
    @patch.object(GeneratorRunner, 'run', return_value=True)
    def test_incremental_generation_fallback_to_full(self, mock_run, tmp_path):
        """Test incremental generation falls back to full when base version unavailable."""
        generator = ClientGenerator(output_base=tmp_path)
        
        new_schema = {
            "openapi": "3.0.0",
            "info": {"title": "Test API", "version": "1.1.0"},
            "paths": {
                "/api/users": {"get": {}}
            }
        }
        
        registry_path = tmp_path / "test_api" / "version_registry.json"
        registry_path.parent.mkdir(parents=True)
        
        versioning = VersioningConfig(
            enabled=True,
            base_version="1.0.0",
            generation_mode="changed"
        )
        
        version_config = VersionConfig(
            version="1.1.0",
            input={"target": "http://test.com/v1.1/openapi.json", "prefer_json": True},
            output={"base_url": "http://test.com", "format_with_black": False}
        )
        
        config = ServiceConfig(
            name="Test API",
            schema_url="http://test.com/openapi.json",
            output_dir="test_api",
            versioning=versioning,
            versions={"1.1.0": version_config}
        )
        
        output_path = tmp_path / "test_api" / "1.1.0"
        
        with patch.object(SchemaProcessor, 'fetch', return_value=new_schema):
            result = generator._generate_versioned_client(
                "test_api", "1.1.0", version_config, output_path, True, config, registry_path
            )
        
        assert result is True
        assert mock_run.called
        # Should have been called with full schema (fallback)
        call_args = mock_run.call_args
        assert call_args is not None
    
    @pytest.mark.integration
    @patch.object(GeneratorRunner, 'run', return_value=True)
    def test_base_version_always_full_generation(self, mock_run, tmp_path):
        """Test that base version always uses full generation even if mode is changed."""
        generator = ClientGenerator(output_base=tmp_path)
        
        schema = {
            "openapi": "3.0.0",
            "info": {"title": "Test API", "version": "1.0.0"},
            "paths": {
                "/api/users": {"get": {}}
            }
        }
        
        registry_path = tmp_path / "test_api" / "version_registry.json"
        registry_path.parent.mkdir(parents=True)
        
        versioning = VersioningConfig(
            enabled=True,
            base_version="1.0.0",
            generation_mode="changed"  # Even though changed, base should be full
        )
        
        version_config = VersionConfig(
            version="1.0.0",
            input={"target": "http://test.com/v1/openapi.json", "prefer_json": True},
            output={"base_url": "http://test.com", "format_with_black": False}
        )
        
        config = ServiceConfig(
            name="Test API",
            schema_url="http://test.com/openapi.json",
            output_dir="test_api",
            versioning=versioning,
            versions={"1.0.0": version_config}
        )
        
        output_path = tmp_path / "test_api" / "1.0.0"
        
        with patch.object(SchemaProcessor, 'fetch', return_value=schema):
            result = generator._generate_versioned_client(
                "test_api", "1.0.0", version_config, output_path, True, config, registry_path
            )
        
        assert result is True
        assert mock_run.called
        # Should use full generation (not incremental)


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

