# -*- coding: utf-8 -*-
"""Location: ./tests/unit/mcpgateway/tools/builder/test_schema.py
Copyright 2026
SPDX-License-Identifier: Apache-2.0
Authors: Teryl Taylor

Unit tests for builder schema validation (Pydantic models).
"""

# Third-Party
import pytest
from pydantic import ValidationError

# First-Party
from mcpgateway.tools.builder.schema import (
    BuildableConfig,
    CertificatesConfig,
    DeploymentConfig,
    GatewayConfig,
    InfrastructureConfig,
    MCPStackConfig,
    PluginConfig,
    PostgresConfig,
    RedisConfig,
)


class TestDeploymentConfig:
    """Test DeploymentConfig validation."""

    def test_valid_kubernetes_deployment(self):
        """Test valid Kubernetes deployment configuration."""
        config = DeploymentConfig(type="kubernetes", namespace="test-ns")
        assert config.type == "kubernetes"
        assert config.namespace == "test-ns"
        assert config.project_name is None

    def test_valid_compose_deployment(self):
        """Test valid Docker Compose deployment configuration."""
        config = DeploymentConfig(type="compose", project_name="test-project")
        assert config.type == "compose"
        assert config.project_name == "test-project"
        assert config.namespace is None

    def test_invalid_deployment_type(self):
        """Test invalid deployment type."""
        with pytest.raises(ValidationError):
            DeploymentConfig(type="invalid")


class TestGatewayConfig:
    """Test GatewayConfig validation."""

    def test_gateway_with_image(self):
        """Test gateway config with pre-built image."""
        config = GatewayConfig(image="mcpgateway:latest", port=4444)
        assert config.image == "mcpgateway:latest"
        assert config.port == 4444
        assert config.repo is None

    def test_gateway_with_repo(self):
        """Test gateway config with repository build."""
        config = GatewayConfig(repo="https://github.com/org/repo.git", ref="main", context=".", port=4444)
        assert config.repo == "https://github.com/org/repo.git"
        assert config.ref == "main"
        assert config.image is None

    def test_gateway_without_image_or_repo(self):
        """Test that gateway requires either image or repo."""
        with pytest.raises(ValueError, match="must specify either 'image' or 'repo'"):
            GatewayConfig(port=4444)

    def test_gateway_defaults(self):
        """Test gateway default values."""
        config = GatewayConfig(image="test:latest")
        assert config.port == 4444
        assert config.mtls_enabled is True
        assert config.ref == "main"
        assert config.context == "."
        assert config.containerfile == "Containerfile"


class TestPluginConfig:
    """Test PluginConfig validation."""

    def test_plugin_with_image(self):
        """Test plugin config with pre-built image."""
        config = PluginConfig(name="TestPlugin", image="test:latest")
        assert config.name == "TestPlugin"
        assert config.image == "test:latest"
        assert config.repo is None

    def test_plugin_with_repo(self):
        """Test plugin config with repository build."""
        config = PluginConfig(name="TestPlugin", repo="https://github.com/org/plugin.git", ref="v1.0.0", context="plugins/test")
        assert config.name == "TestPlugin"
        assert config.repo == "https://github.com/org/plugin.git"
        assert config.ref == "v1.0.0"
        assert config.context == "plugins/test"

    def test_plugin_without_name(self):
        """Test that plugin requires name."""
        with pytest.raises(ValidationError):
            PluginConfig(image="test:latest")

    def test_plugin_empty_name(self):
        """Test that plugin name cannot be empty."""
        with pytest.raises(ValidationError, match="Plugin name cannot be empty"):
            PluginConfig(name="", image="test:latest")

    def test_plugin_whitespace_name(self):
        """Test that plugin name cannot be whitespace only."""
        with pytest.raises(ValidationError, match="Plugin name cannot be empty"):
            PluginConfig(name="   ", image="test:latest")

    def test_plugin_name_rejects_unsafe_characters(self):
        """Test that plugin name rejects shell-like characters."""
        with pytest.raises(ValidationError, match="must contain only letters, numbers, underscore, and hyphen"):
            PluginConfig(name="bad$(name)", image="test:latest")

    def test_plugin_defaults(self):
        """Test plugin default values."""
        config = PluginConfig(name="TestPlugin", image="test:latest")
        assert config.port == 8000
        assert config.expose_port is False
        assert config.mtls_enabled is True
        assert config.plugin_overrides == {}

    def test_plugin_overrides(self):
        """Test plugin with overrides."""
        config = PluginConfig(name="TestPlugin", image="test:latest", plugin_overrides={"priority": 10, "mode": "enforce", "tags": ["security", "filter"]})
        assert config.plugin_overrides["priority"] == 10
        assert config.plugin_overrides["mode"] == "enforce"
        assert config.plugin_overrides["tags"] == ["security", "filter"]


class TestCertificatesConfig:
    """Test CertificatesConfig validation."""

    def test_certificates_defaults(self):
        """Test certificates default values."""
        config = CertificatesConfig()
        assert config.validity_days == 825
        assert config.auto_generate is True
        assert config.ca_path == "./certs/mcp/ca"
        assert config.gateway_path == "./certs/mcp/gateway"
        assert config.plugins_path == "./certs/mcp/plugins"

    def test_certificates_custom_values(self):
        """Test certificates with custom values."""
        config = CertificatesConfig(validity_days=365, auto_generate=False, ca_path="/custom/ca", gateway_path="/custom/gateway", plugins_path="/custom/plugins")
        assert config.validity_days == 365
        assert config.auto_generate is False
        assert config.ca_path == "/custom/ca"


class TestInfrastructureConfig:
    """Test InfrastructureConfig validation."""

    def test_postgres_defaults(self):
        """Test PostgreSQL default configuration."""
        config = PostgresConfig()
        assert config.enabled is True
        assert config.image == "quay.io/sclorg/postgresql-15-c9s:latest"
        assert config.database == "mcp"
        assert config.user == "postgres"
        assert config.password == "mysecretpassword"
        assert config.storage_size == "10Gi"

    def test_postgres_custom(self):
        """Test PostgreSQL custom configuration."""
        config = PostgresConfig(enabled=True, image="postgres:16", database="customdb", user="customuser", password="custompass", storage_size="20Gi", storage_class="fast-ssd")
        assert config.image == "postgres:16"
        assert config.database == "customdb"
        assert config.storage_class == "fast-ssd"

    def test_redis_defaults(self):
        """Test Redis default configuration."""
        config = RedisConfig()
        assert config.enabled is True
        assert config.image == "redis:latest"

    def test_infrastructure_defaults(self):
        """Test infrastructure with default values."""
        config = InfrastructureConfig()
        assert config.postgres.enabled is True
        assert config.redis.enabled is True


class TestMCPStackConfig:
    """Test complete MCPStackConfig validation."""

    def test_minimal_config(self):
        """Test minimal valid configuration."""
        config = MCPStackConfig(deployment=DeploymentConfig(type="compose", project_name="test"), gateway=GatewayConfig(image="mcpgateway:latest"))
        assert config.deployment.type == "compose"
        assert config.gateway.image == "mcpgateway:latest"
        assert config.plugins == []

    def test_full_config(self):
        """Test full configuration with all options."""
        config = MCPStackConfig(
            deployment=DeploymentConfig(type="kubernetes", namespace="prod"),
            gateway=GatewayConfig(image="mcpgateway:latest", port=4444, mtls_enabled=True),
            plugins=[PluginConfig(name="Plugin1", image="plugin1:latest"), PluginConfig(name="Plugin2", image="plugin2:latest")],
            certificates=CertificatesConfig(validity_days=365),
            infrastructure=InfrastructureConfig(),
        )
        assert config.deployment.namespace == "prod"
        assert len(config.plugins) == 2
        assert config.certificates.validity_days == 365

    def test_duplicate_plugin_names(self):
        """Test that duplicate plugin names are rejected."""
        with pytest.raises(ValidationError, match="Duplicate plugin names found"):
            MCPStackConfig(
                deployment=DeploymentConfig(type="compose"),
                gateway=GatewayConfig(image="test:latest"),
                plugins=[PluginConfig(name="DuplicatePlugin", image="plugin1:latest"), PluginConfig(name="DuplicatePlugin", image="plugin2:latest")],
            )

    def test_unique_plugin_names(self):
        """Test that unique plugin names are accepted."""
        config = MCPStackConfig(
            deployment=DeploymentConfig(type="compose"),
            gateway=GatewayConfig(image="test:latest"),
            plugins=[PluginConfig(name="Plugin1", image="plugin1:latest"), PluginConfig(name="Plugin2", image="plugin2:latest"), PluginConfig(name="Plugin3", image="plugin3:latest")],
        )
        assert len(config.plugins) == 3
        assert [p.name for p in config.plugins] == ["Plugin1", "Plugin2", "Plugin3"]

    def test_config_with_repo_builds(self):
        """Test configuration with repository builds."""
        config = MCPStackConfig(
            deployment=DeploymentConfig(type="compose"),
            gateway=GatewayConfig(repo="https://github.com/org/gateway.git", ref="v2.0.0"),
            plugins=[PluginConfig(name="BuiltPlugin", repo="https://github.com/org/plugin.git", ref="main", context="plugins/src")],
        )
        assert config.gateway.repo is not None
        assert config.gateway.ref == "v2.0.0"
        assert config.plugins[0].repo is not None
        assert config.plugins[0].context == "plugins/src"


class TestBuildableConfig:
    """Test BuildableConfig base class validation."""

    def test_mtls_defaults(self):
        """Test mTLS default settings."""
        config = GatewayConfig(image="test:latest")
        assert config.mtls_enabled is True

    def test_mtls_disabled(self):
        """Test mTLS can be disabled."""
        config = GatewayConfig(image="test:latest", mtls_enabled=False)
        assert config.mtls_enabled is False

    def test_env_vars(self):
        """Test environment variables."""
        config = GatewayConfig(image="test:latest", env_vars={"LOG_LEVEL": "DEBUG", "PORT": "4444"})
        assert config.env_vars["LOG_LEVEL"] == "DEBUG"
        assert config.env_vars["PORT"] == "4444"

    def test_multi_stage_build(self):
        """Test multi-stage build target."""
        config = PluginConfig(name="TestPlugin", repo="https://github.com/org/plugin.git", containerfile="Dockerfile", target="production")
        assert config.containerfile == "Dockerfile"
        assert config.target == "production"

    def test_repo_rejects_unsafe_characters(self):
        """Repository URL should reject command-like metacharacters."""
        with pytest.raises(ValidationError, match="unsupported characters"):
            GatewayConfig(repo="https://github.com/org/repo.git$(whoami)")

    def test_repo_rejects_backslashes(self):
        """Repository URL should reject backslashes in SCP-style values."""
        with pytest.raises(ValidationError, match="cannot contain backslashes"):
            GatewayConfig(repo=r"git@github.com:org/repo\\.git")

        with pytest.raises(ValidationError, match="cannot contain backslashes"):
            GatewayConfig(repo=r"git@github.com:org/repo\\xgit")

    def test_repo_accepts_scp_style_git_url(self):
        """Repository URL should support git@host:path syntax."""
        config = GatewayConfig(repo="git@github.com:org/repo.git")
        assert config.repo == "git@github.com:org/repo.git"

    def test_ref_rejects_unsafe_sequences(self):
        """Git ref should reject unsafe option-like or traversal patterns."""
        with pytest.raises(ValidationError, match="cannot start with '-'"):
            GatewayConfig(repo="https://github.com/org/repo.git", ref="-badref")

        with pytest.raises(ValidationError, match="unsupported sequence"):
            GatewayConfig(repo="https://github.com/org/repo.git", ref="main..evil")

        with pytest.raises(ValidationError, match="unsupported sequence"):
            GatewayConfig(repo="https://github.com/org/repo.git", ref="main@{1}")

        with pytest.raises(ValidationError, match="unsupported sequence"):
            GatewayConfig(repo="https://github.com/org/repo.git", ref="main//evil")
