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

Unit tests for builder common utilities.
"""

# Standard
import os
from pathlib import Path
import shutil
import subprocess
from unittest.mock import MagicMock, Mock, patch
from mcpgateway.tools.builder.schema import MCPStackConfig

# Third-Party
import pytest
import yaml

# First-Party
from mcpgateway.tools.builder.common import (
    copy_env_template,
    deploy_compose,
    deploy_kubernetes,
    destroy_compose,
    destroy_kubernetes,
    generate_compose_manifests,
    generate_kubernetes_manifests,
    generate_plugin_config,
    get_deploy_dir,
    get_docker_compose_command,
    load_config,
    run_compose,
    verify_compose,
    verify_kubernetes,
)


class TestGetDeployDir:
    """Test get_deploy_dir function."""

    def test_default_deploy_dir(self):
        """Test default deploy directory."""
        with patch.dict(os.environ, {}, clear=True):
            result = get_deploy_dir()
            assert result == Path("./deploy")

    def test_custom_deploy_dir(self):
        """Test custom deploy directory from environment variable."""
        with patch.dict(os.environ, {"MCP_DEPLOY_DIR": "/custom/deploy"}):
            result = get_deploy_dir()
            assert result == Path("/custom/deploy")


class TestLoadConfig:
    """Test load_config function."""

    def test_load_valid_config(self, tmp_path):
        """Test loading valid YAML configuration."""
        config_file = tmp_path / "mcp-stack.yaml"
        config_data = {
            "deployment": {"type": "compose", "project_name": "test"},
            "gateway": {"image": "mcpgateway:latest"},
            "plugins": [],
        }
        config_file.write_text(yaml.dump(config_data))

        result = load_config(str(config_file))
        assert result.deployment.type == "compose"
        assert result.gateway.image == "mcpgateway:latest"

    def test_load_nonexistent_config(self):
        """Test loading non-existent configuration file."""
        with pytest.raises(FileNotFoundError, match="Configuration file not found"):
            load_config("/nonexistent/config.yaml")


class TestGeneratePluginConfig:
    """Test generate_plugin_config function."""

    @patch("mcpgateway.tools.builder.common.Environment")
    def test_generate_plugin_config_compose(self, mock_env_class, tmp_path):
        """Test generating plugin config for Docker Compose deployment."""
        # Setup mock template
        mock_template = MagicMock()
        mock_template.render.return_value = "plugins:\n  - name: TestPlugin\n"
        mock_env = MagicMock()
        mock_env.get_template.return_value = mock_template
        mock_env_class.return_value = mock_env

        # Create fake template directory
        template_dir = tmp_path / "templates"
        template_dir.mkdir()

        config = MCPStackConfig.model_validate(
            {
                "gateway": {"image": "mcpgateway:latest"},
                "deployment": {"type": "compose"},
                "plugins": [{"name": "TestPlugin", "port": 8000, "mtls_enabled": True, "repo": "https://github.com/test/plugin.git"}],
            }
        )

        with patch("mcpgateway.tools.builder.common.Path") as mock_path:
            mock_path.return_value.__truediv__.return_value = template_dir
            output_dir = tmp_path / "output"
            output_dir.mkdir()

            result = generate_plugin_config(config, output_dir)

            # Verify template was called
            mock_env.get_template.assert_called_once_with("plugins-config.yaml.j2")
            assert result == output_dir / "plugins-config.yaml"

    @patch("mcpgateway.tools.builder.common.Environment")
    def test_generate_plugin_config_kubernetes(self, mock_env_class, tmp_path):
        """Test generating plugin config for Kubernetes deployment."""
        # Setup mock template
        mock_template = MagicMock()
        mock_template.render.return_value = "plugins:\n  - name: TestPlugin\n"
        mock_env = MagicMock()
        mock_env.get_template.return_value = mock_template
        mock_env_class.return_value = mock_env

        # Create fake template directory
        template_dir = tmp_path / "templates"
        template_dir.mkdir()

        config = MCPStackConfig.model_validate(
            {
                "gateway": {"image": "mcpgateway:latest"},
                "deployment": {"type": "kubernetes", "namespace": "test-ns"},
                "plugins": [{"name": "TestPlugin", "port": 8000, "mtls_enabled": False, "repo": "https://github.com/test/plugin1.git"}],
            }
        )

        with patch("mcpgateway.tools.builder.common.Path") as mock_path:
            mock_path.return_value.__truediv__.return_value = template_dir
            output_dir = tmp_path / "output"
            output_dir.mkdir()

            result = generate_plugin_config(config, output_dir)

            # Verify template was called
            assert mock_env.get_template.called
            assert result == output_dir / "plugins-config.yaml"

    @patch("mcpgateway.tools.builder.common.Environment")
    def test_generate_plugin_config_with_overrides(self, mock_env_class, tmp_path):
        """Test generating plugin config with plugin_overrides."""
        # Setup mock template
        mock_template = MagicMock()
        mock_template.render.return_value = "plugins:\n  - name: TestPlugin\n"
        mock_env = MagicMock()
        mock_env.get_template.return_value = mock_template
        mock_env_class.return_value = mock_env

        # Create fake template directory
        template_dir = tmp_path / "templates"
        template_dir.mkdir()

        config = MCPStackConfig.model_validate(
            {
                "deployment": {"type": "compose"},
                "gateway": {"image": "mcpgateway:latest"},
                "plugins": [
                    {
                        "name": "TestPlugin",
                        "port": 8000,
                        "plugin_overrides": {
                            "priority": 10,
                            "mode": "enforce",
                            "tags": ["security"],
                        },
                        "repo": "https://github.com/test/plugin1.git",
                    }
                ],
            }
        )

        with patch("mcpgateway.tools.builder.common.Path") as mock_path:
            mock_path.return_value.__truediv__.return_value = template_dir
            output_dir = tmp_path / "output"
            output_dir.mkdir()

            result = generate_plugin_config(config, output_dir)
            assert result == output_dir / "plugins-config.yaml"


class TestCopyEnvTemplate:
    """Test copy_env_template function."""

    def test_copy_env_template_success(self, tmp_path):
        """Test successful copying of .env.template."""
        # Create plugin build dir with .env.template
        plugin_dir = tmp_path / "plugin"
        plugin_dir.mkdir()
        template_file = plugin_dir / ".env.template"
        template_file.write_text("TEST_VAR=value\n")

        # Setup deploy dir
        deploy_dir = tmp_path / "deploy"

        with patch("mcpgateway.tools.builder.common.get_deploy_dir", return_value=deploy_dir):
            copy_env_template("TestPlugin", plugin_dir)

            target_file = deploy_dir / "env" / ".env.TestPlugin"
            assert target_file.exists()
            assert target_file.read_text() == "TEST_VAR=value\n"

    def test_copy_env_template_no_template(self, tmp_path):
        """Test when .env.template doesn't exist."""
        plugin_dir = tmp_path / "plugin"
        plugin_dir.mkdir()

        deploy_dir = tmp_path / "deploy"

        with patch("mcpgateway.tools.builder.common.get_deploy_dir", return_value=deploy_dir):
            # Should not raise error, just skip
            copy_env_template("TestPlugin", plugin_dir, verbose=True)

    def test_copy_env_template_target_exists(self, tmp_path):
        """Test when target file already exists."""
        # Create plugin build dir with .env.template
        plugin_dir = tmp_path / "plugin"
        plugin_dir.mkdir()
        template_file = plugin_dir / ".env.template"
        template_file.write_text("NEW_VAR=newvalue\n")

        # Setup deploy dir with existing target
        deploy_dir = tmp_path / "deploy"
        deploy_dir.mkdir()
        env_dir = deploy_dir / "env"
        env_dir.mkdir()
        target_file = env_dir / ".env.TestPlugin"
        target_file.write_text("OLD_VAR=oldvalue\n")

        with patch("mcpgateway.tools.builder.common.get_deploy_dir", return_value=deploy_dir):
            copy_env_template("TestPlugin", plugin_dir)

            # Should not overwrite
            assert target_file.read_text() == "OLD_VAR=oldvalue\n"


class TestGetDockerComposeCommand:
    """Test get_docker_compose_command function."""

    @patch("mcpgateway.tools.builder.common.shutil.which")
    @patch("mcpgateway.tools.builder.common.subprocess.run")
    def test_docker_compose_plugin(self, mock_run, mock_which):
        """Test detecting docker compose plugin."""
        mock_which.return_value = "/usr/bin/docker"
        mock_run.return_value = Mock(returncode=0)

        result = get_docker_compose_command()
        assert result == ["docker", "compose"]

    @patch("mcpgateway.tools.builder.common.shutil.which")
    @patch("mcpgateway.tools.builder.common.subprocess.run")
    def test_docker_compose_standalone(self, mock_run, mock_which):
        """Test detecting standalone docker-compose."""

        def which_side_effect(cmd):
            if cmd == "docker":
                return "/usr/bin/docker"
            elif cmd == "docker-compose":
                return "/usr/bin/docker-compose"
            return None

        mock_which.side_effect = which_side_effect
        mock_run.side_effect = subprocess.CalledProcessError(1, "cmd")

        result = get_docker_compose_command()
        assert result == ["docker-compose"]

    @patch("mcpgateway.tools.builder.common.shutil.which")
    def test_docker_compose_not_found(self, mock_which):
        """Test when docker compose is not available."""
        mock_which.return_value = None

        with pytest.raises(RuntimeError, match="Docker Compose not found"):
            get_docker_compose_command()


class TestRunCompose:
    """Test run_compose function."""

    @patch("mcpgateway.tools.builder.common.get_docker_compose_command")
    @patch("mcpgateway.tools.builder.common.subprocess.run")
    def test_run_compose_success(self, mock_run, mock_get_cmd, tmp_path):
        """Test successful compose command execution."""
        compose_file = tmp_path / "docker-compose.yaml"
        compose_file.write_text("services:\n  test: {}\n")

        mock_get_cmd.return_value = ["docker", "compose"]
        mock_run.return_value = Mock(returncode=0, stdout="Success", stderr="")

        result = run_compose(compose_file, ["ps"])
        assert result.returncode == 0
        mock_run.assert_called_once()

    @patch("mcpgateway.tools.builder.common.get_docker_compose_command")
    def test_run_compose_file_not_found(self, mock_get_cmd, tmp_path):
        """Test run_compose with non-existent file."""
        compose_file = tmp_path / "nonexistent.yaml"
        mock_get_cmd.return_value = ["docker", "compose"]

        with pytest.raises(FileNotFoundError, match="Compose file not found"):
            run_compose(compose_file, ["ps"])

    @patch("mcpgateway.tools.builder.common.get_docker_compose_command")
    @patch("mcpgateway.tools.builder.common.subprocess.run")
    def test_run_compose_command_failure(self, mock_run, mock_get_cmd, tmp_path):
        """Test run_compose command failure."""
        compose_file = tmp_path / "docker-compose.yaml"
        compose_file.write_text("services:\n  test: {}\n")

        mock_get_cmd.return_value = ["docker", "compose"]
        mock_run.side_effect = subprocess.CalledProcessError(1, "cmd", output="", stderr="Error")

        with pytest.raises(RuntimeError, match="Docker Compose failed"):
            run_compose(compose_file, ["up", "-d"])


class TestDeployCompose:
    """Test deploy_compose function."""

    @patch("mcpgateway.tools.builder.common.run_compose")
    def test_deploy_compose_success(self, mock_run, tmp_path):
        """Test successful Docker Compose deployment."""
        compose_file = tmp_path / "docker-compose.yaml"
        mock_run.return_value = Mock(stdout="Deployed", stderr="")

        deploy_compose(compose_file)
        mock_run.assert_called_once_with(compose_file, ["up", "-d"], verbose=False)


class TestVerifyCompose:
    """Test verify_compose function."""

    @patch("mcpgateway.tools.builder.common.run_compose")
    def test_verify_compose(self, mock_run, tmp_path):
        """Test verifying Docker Compose deployment."""
        compose_file = tmp_path / "docker-compose.yaml"
        mock_run.return_value = Mock(stdout="test-service running", stderr="")

        result = verify_compose(compose_file)
        assert "test-service running" in result
        mock_run.assert_called_once_with(compose_file, ["ps"], verbose=False, check=False)


class TestDestroyCompose:
    """Test destroy_compose function."""

    @patch("mcpgateway.tools.builder.common.run_compose")
    def test_destroy_compose_success(self, mock_run, tmp_path):
        """Test successful Docker Compose destruction."""
        compose_file = tmp_path / "docker-compose.yaml"
        compose_file.write_text("services:\n  test: {}\n")
        mock_run.return_value = Mock(stdout="Removed", stderr="")

        destroy_compose(compose_file)
        mock_run.assert_called_once_with(compose_file, ["down", "-v"], verbose=False)

    def test_destroy_compose_file_not_found(self, tmp_path):
        """Test destroying with non-existent compose file."""
        compose_file = tmp_path / "nonexistent.yaml"

        # Should not raise error, just print warning
        destroy_compose(compose_file)


class TestDeployKubernetes:
    """Test deploy_kubernetes function."""

    @patch("mcpgateway.tools.builder.common.shutil.which")
    @patch("mcpgateway.tools.builder.common.subprocess.run")
    def test_deploy_kubernetes_success(self, mock_run, mock_which, tmp_path):
        """Test successful Kubernetes deployment."""
        mock_which.return_value = "/usr/bin/kubectl"
        mock_run.return_value = Mock(returncode=0, stdout="created", stderr="")

        manifests_dir = tmp_path / "manifests"
        manifests_dir.mkdir()
        (manifests_dir / "gateway-deployment.yaml").write_text("apiVersion: v1\n")
        (manifests_dir / "plugins-config.yaml").write_text("plugins: []\n")

        deploy_kubernetes(manifests_dir)
        assert mock_run.called

    @patch("mcpgateway.tools.builder.common.shutil.which")
    def test_deploy_kubernetes_kubectl_not_found(self, mock_which, tmp_path):
        """Test deployment when kubectl is not available."""
        mock_which.return_value = None
        manifests_dir = tmp_path / "manifests"

        with pytest.raises(RuntimeError, match="kubectl not found"):
            deploy_kubernetes(manifests_dir)

    @patch("mcpgateway.tools.builder.common.shutil.which")
    @patch("mcpgateway.tools.builder.common.subprocess.run")
    def test_deploy_kubernetes_with_certs(self, mock_run, mock_which, tmp_path):
        """Test Kubernetes deployment with certificate secrets."""
        mock_which.return_value = "/usr/bin/kubectl"
        mock_run.return_value = Mock(returncode=0, stdout="created", stderr="")

        manifests_dir = tmp_path / "manifests"
        manifests_dir.mkdir()
        (manifests_dir / "gateway-deployment.yaml").write_text("apiVersion: v1\n")
        (manifests_dir / "cert-secrets.yaml").write_text("apiVersion: v1\n")

        deploy_kubernetes(manifests_dir)
        assert mock_run.called


class TestVerifyKubernetes:
    """Test verify_kubernetes function."""

    @patch("mcpgateway.tools.builder.common.shutil.which")
    @patch("mcpgateway.tools.builder.common.subprocess.run")
    def test_verify_kubernetes_success(self, mock_run, mock_which):
        """Test successful Kubernetes verification."""
        mock_which.return_value = "/usr/bin/kubectl"
        mock_run.return_value = Mock(returncode=0, stdout="pod-1 Running\npod-2 Running", stderr="")

        result = verify_kubernetes("test-ns")
        assert "Running" in result
        mock_run.assert_called_once()

    @patch("mcpgateway.tools.builder.common.shutil.which")
    def test_verify_kubernetes_kubectl_not_found(self, mock_which):
        """Test verification when kubectl is not available."""
        mock_which.return_value = None

        with pytest.raises(RuntimeError, match="kubectl not found"):
            verify_kubernetes("test-ns")

    @patch("mcpgateway.tools.builder.common.shutil.which")
    @patch("mcpgateway.tools.builder.common.subprocess.run")
    def test_verify_kubernetes_with_wait(self, mock_run, mock_which):
        """Test Kubernetes verification with wait."""
        mock_which.return_value = "/usr/bin/kubectl"
        mock_run.return_value = Mock(returncode=0, stdout="Ready", stderr="")

        result = verify_kubernetes("test-ns", wait=True, timeout=60)
        assert mock_run.call_count >= 1


class TestDestroyKubernetes:
    """Test destroy_kubernetes function."""

    @patch("mcpgateway.tools.builder.common.shutil.which")
    @patch("mcpgateway.tools.builder.common.subprocess.run")
    def test_destroy_kubernetes_success(self, mock_run, mock_which, tmp_path):
        """Test successful Kubernetes destruction."""
        mock_which.return_value = "/usr/bin/kubectl"
        mock_run.return_value = Mock(returncode=0, stdout="deleted", stderr="")

        manifests_dir = tmp_path / "manifests"
        manifests_dir.mkdir()
        (manifests_dir / "gateway-deployment.yaml").write_text("apiVersion: v1\n")
        (manifests_dir / "plugins-config.yaml").write_text("plugins: []\n")

        destroy_kubernetes(manifests_dir)
        assert mock_run.called

    @patch("mcpgateway.tools.builder.common.shutil.which")
    def test_destroy_kubernetes_kubectl_not_found(self, mock_which, tmp_path):
        """Test destruction when kubectl is not available."""
        mock_which.return_value = None
        manifests_dir = tmp_path / "manifests"

        with pytest.raises(RuntimeError, match="kubectl not found"):
            destroy_kubernetes(manifests_dir)

    def test_destroy_kubernetes_dir_not_found(self, tmp_path):
        """Test destroying with non-existent manifests directory."""
        manifests_dir = tmp_path / "nonexistent"

        with patch("mcpgateway.tools.builder.common.shutil.which", return_value="/usr/bin/kubectl"):
            # Should not raise error, just print warning
            destroy_kubernetes(manifests_dir)


class TestGenerateKubernetesManifests:
    """Test generate_kubernetes_manifests function with real template rendering."""

    def test_generate_manifests_gateway_only(self, tmp_path):
        """Test generating Kubernetes manifests for gateway only."""
        output_dir = tmp_path / "manifests"
        output_dir.mkdir()

        config = MCPStackConfig.model_validate(
            {
                "deployment": {"type": "kubernetes", "namespace": "test-ns"},
                "gateway": {
                    "image": "mcpgateway:latest",
                    "port": 4444,
                    "mtls_enabled": False,
                },
                "plugins": [],
            }
        )

        generate_kubernetes_manifests(config, output_dir)

        # Verify gateway deployment was created
        gateway_file = output_dir / "gateway-deployment.yaml"
        assert gateway_file.exists()

        # Parse and validate YAML
        with open(gateway_file) as f:
            docs = list(yaml.safe_load_all(f))

        # Should have Deployment and Service
        assert len(docs) >= 2

        # Validate Deployment
        deployment = next((d for d in docs if d.get("kind") == "Deployment"), None)
        assert deployment is not None
        assert deployment["metadata"]["name"] == "mcpgateway"
        assert deployment["metadata"]["namespace"] == "test-ns"
        assert deployment["spec"]["template"]["spec"]["containers"][0]["image"] == "mcpgateway:latest"

        # Validate Service
        service = next((d for d in docs if d.get("kind") == "Service"), None)
        assert service is not None
        assert service["metadata"]["name"] == "mcpgateway"
        assert service["spec"]["ports"][0]["port"] == 4444

    def test_generate_manifests_with_plugins(self, tmp_path):
        """Test generating Kubernetes manifests with plugins."""
        output_dir = tmp_path / "manifests"
        output_dir.mkdir()

        config = MCPStackConfig.model_validate(
            {
                "deployment": {"type": "kubernetes", "namespace": "mcp-test"},
                "gateway": {
                    "image": "mcpgateway:latest",
                    "port": 4444,
                    "mtls_enabled": False,
                },
                "plugins": [
                    {
                        "name": "TestPlugin",
                        "image": "test-plugin:v1",
                        "port": 8000,
                        "mtls_enabled": False,
                    },
                    {
                        "name": "AnotherPlugin",
                        "image": "another-plugin:v2",
                        "port": 8001,
                        "mtls_enabled": False,
                    },
                ],
            }
        )

        generate_kubernetes_manifests(config, output_dir)

        # Verify plugin deployments were created
        plugin1_file = output_dir / "plugin-testplugin-deployment.yaml"
        plugin2_file = output_dir / "plugin-anotherplugin-deployment.yaml"

        assert plugin1_file.exists()
        assert plugin2_file.exists()

        # Parse and validate first plugin
        with open(plugin1_file) as f:
            docs = list(yaml.safe_load_all(f))

        deployment = next((d for d in docs if d.get("kind") == "Deployment"), None)
        assert deployment is not None
        assert deployment["metadata"]["name"] == "mcp-plugin-testplugin"
        assert deployment["metadata"]["namespace"] == "mcp-test"
        assert deployment["spec"]["template"]["spec"]["containers"][0]["image"] == "test-plugin:v1"

    def test_generate_manifests_with_mtls(self, tmp_path):
        """Test generating Kubernetes manifests with mTLS enabled."""
        # Change to tmp_path to ensure we have a valid working directory
        original_dir = None
        try:
            original_dir = os.getcwd()
        except (FileNotFoundError, OSError):
            pass  # Current directory doesn't exist

        os.chdir(tmp_path)

        try:
            output_dir = tmp_path / "manifests"
            output_dir.mkdir()

            # Create fake certificate files in the actual location where the code looks
            certs_dir = Path("certs/mcp")
            ca_dir = certs_dir / "ca"
            gateway_dir = certs_dir / "gateway"
            plugin_dir = certs_dir / "plugins" / "SecurePlugin"

            ca_dir.mkdir(parents=True, exist_ok=True)
            gateway_dir.mkdir(parents=True, exist_ok=True)
            plugin_dir.mkdir(parents=True, exist_ok=True)

            (ca_dir / "ca.crt").write_bytes(b"fake-ca-cert")
            (gateway_dir / "client.crt").write_bytes(b"fake-gateway-cert")
            (gateway_dir / "client.key").write_bytes(b"fake-gateway-key")
            (plugin_dir / "server.crt").write_bytes(b"fake-plugin-cert")
            (plugin_dir / "server.key").write_bytes(b"fake-plugin-key")

            config = MCPStackConfig.model_validate(
                {
                    "deployment": {"type": "kubernetes", "namespace": "secure-ns"},
                    "gateway": {
                        "image": "mcpgateway:latest",
                        "port": 4444,
                        "mtls_enabled": True,
                    },
                    "plugins": [
                        {
                            "name": "SecurePlugin",
                            "image": "secure-plugin:v1",
                            "port": 8000,
                            "mtls_enabled": True,
                        }
                    ],
                }
            )

            generate_kubernetes_manifests(config, output_dir)
        finally:
            # Clean up created certificate files
            if Path("certs").exists():
                shutil.rmtree("certs")

            # Restore original directory if it exists
            if original_dir and Path(original_dir).exists():
                os.chdir(original_dir)

        # Verify certificate secrets were created
        cert_secrets_file = output_dir / "cert-secrets.yaml"
        assert cert_secrets_file.exists()

        # Parse and validate secrets
        with open(cert_secrets_file) as f:
            docs = list(yaml.safe_load_all(f))

        # Should have secrets for CA, gateway, and plugin
        secrets = [d for d in docs if d.get("kind") == "Secret"]
        assert len(secrets) >= 2  # At least gateway and plugin secrets

    def test_generate_manifests_with_infrastructure(self, tmp_path):
        """Test generating Kubernetes manifests with PostgreSQL and Redis."""
        output_dir = tmp_path / "manifests"
        output_dir.mkdir()

        config = MCPStackConfig.model_validate(
            {
                "deployment": {"type": "kubernetes", "namespace": "infra-ns"},
                "gateway": {
                    "image": "mcpgateway:latest",
                    "port": 4444,
                    "mtls_enabled": False,
                },
                "plugins": [],
                "infrastructure": {
                    "postgres": {
                        "enabled": True,
                        "image": "postgres:17",
                        "database": "testdb",
                        "user": "testuser",
                        "password": "testpass",
                    },
                    "redis": {
                        "enabled": True,
                        "image": "redis:alpine",
                    },
                },
            }
        )

        generate_kubernetes_manifests(config, output_dir)

        # Verify infrastructure manifests were created
        postgres_file = output_dir / "postgres-deployment.yaml"
        redis_file = output_dir / "redis-deployment.yaml"

        assert postgres_file.exists()
        assert redis_file.exists()

        # Parse and validate PostgreSQL
        with open(postgres_file) as f:
            docs = list(yaml.safe_load_all(f))

        postgres_deployment = next((d for d in docs if d.get("kind") == "Deployment"), None)
        assert postgres_deployment is not None
        assert postgres_deployment["metadata"]["name"] == "postgres"
        assert postgres_deployment["spec"]["template"]["spec"]["containers"][0]["image"] == "postgres:17"

        # Parse and validate Redis
        with open(redis_file) as f:
            docs = list(yaml.safe_load_all(f))

        redis_deployment = next((d for d in docs if d.get("kind") == "Deployment"), None)
        assert redis_deployment is not None
        assert redis_deployment["metadata"]["name"] == "redis"

        # Verify gateway has database environment variables in Secret
        gateway_file = output_dir / "gateway-deployment.yaml"
        with open(gateway_file) as f:
            docs = list(yaml.safe_load_all(f))

        # Find the Secret containing environment variables
        secret = next((d for d in docs if d.get("kind") == "Secret" and d["metadata"]["name"] == "mcpgateway-env"), None)
        assert secret is not None
        assert "stringData" in secret

        string_data = secret["stringData"]

        # Check DATABASE_URL is set (uses postgresql+psycopg:// for psycopg3)
        assert "DATABASE_URL" in string_data
        assert "postgresql+psycopg://" in string_data["DATABASE_URL"]
        assert "testuser:testpass" in string_data["DATABASE_URL"]

        # Check REDIS_URL is set
        assert "REDIS_URL" in string_data
        assert "redis://redis:6379" in string_data["REDIS_URL"]

        # Verify deployment references the Secret via envFrom
        gateway_deployment = next((d for d in docs if d.get("kind") == "Deployment"), None)
        assert gateway_deployment is not None
        env_from = gateway_deployment["spec"]["template"]["spec"]["containers"][0]["envFrom"]
        assert any(ref.get("secretRef", {}).get("name") == "mcpgateway-env" for ref in env_from)


class TestGenerateComposeManifests:
    """Test generate_compose_manifests function with real template rendering."""

    def test_generate_compose_gateway_only(self, tmp_path):
        """Test generating Docker Compose manifest for gateway only."""
        output_dir = tmp_path / "manifests"
        output_dir.mkdir()

        config = MCPStackConfig.model_validate(
            {
                "deployment": {"type": "compose", "project_name": "test-mcp"},
                "gateway": {
                    "image": "mcpgateway:latest",
                    "port": 4444,
                    "host_port": 4444,
                    "mtls_enabled": False,
                },
                "plugins": [],
            }
        )

        with patch("mcpgateway.tools.builder.common.Path.cwd", return_value=tmp_path):
            generate_compose_manifests(config, output_dir)

        # Verify compose file was created
        compose_file = output_dir / "docker-compose.yaml"
        assert compose_file.exists()

        # Parse and validate
        with open(compose_file) as f:
            compose_data = yaml.safe_load(f)

        assert "services" in compose_data
        assert "mcpgateway" in compose_data["services"]

        gateway = compose_data["services"]["mcpgateway"]
        assert gateway["image"] == "mcpgateway:latest"
        assert gateway["ports"] == ["4444:4444"]

    def test_generate_compose_with_plugins(self, tmp_path):
        """Test generating Docker Compose manifest with plugins."""
        output_dir = tmp_path / "manifests"
        output_dir.mkdir()

        config = MCPStackConfig.model_validate(
            {
                "deployment": {"type": "compose", "project_name": "mcp-stack"},
                "gateway": {
                    "image": "mcpgateway:latest",
                    "port": 4444,
                    "host_port": 4444,
                    "mtls_enabled": False,
                },
                "plugins": [
                    {
                        "name": "Plugin1",
                        "image": "plugin1:v1",
                        "port": 8000,
                        "expose_port": True,
                        "host_port": 8000,
                        "mtls_enabled": False,
                    },
                    {
                        "name": "Plugin2",
                        "image": "plugin2:v1",
                        "port": 8001,
                        "expose_port": False,
                        "mtls_enabled": False,
                    },
                ],
            }
        )

        with patch("mcpgateway.tools.builder.common.Path.cwd", return_value=tmp_path):
            generate_compose_manifests(config, output_dir)

        # Parse and validate
        compose_file = output_dir / "docker-compose.yaml"
        with open(compose_file) as f:
            compose_data = yaml.safe_load(f)

        # Verify plugins are in services
        assert "plugin1" in compose_data["services"]
        assert "plugin2" in compose_data["services"]

        plugin1 = compose_data["services"]["plugin1"]
        assert plugin1["image"] == "plugin1:v1"
        assert "8000:8000" in plugin1["ports"]  # Exposed

        plugin2 = compose_data["services"]["plugin2"]
        assert plugin2["image"] == "plugin2:v1"
        # Plugin2 should not have host port mapping since expose_port is False

    def test_generate_compose_with_mtls(self, tmp_path):
        """Test generating Docker Compose manifest with mTLS certificates."""
        output_dir = tmp_path / "manifests"
        output_dir.mkdir()

        # Create fake certificate structure
        certs_dir = tmp_path / "certs" / "mcp"
        ca_dir = certs_dir / "ca"
        gateway_dir = certs_dir / "gateway"
        plugin_dir = certs_dir / "plugins" / "SecurePlugin"

        ca_dir.mkdir(parents=True)
        gateway_dir.mkdir(parents=True)
        plugin_dir.mkdir(parents=True)

        (ca_dir / "ca.crt").write_text("fake-ca")
        (gateway_dir / "client.crt").write_text("fake-cert")
        (gateway_dir / "client.key").write_text("fake-key")
        (plugin_dir / "server.crt").write_text("fake-plugin-cert")
        (plugin_dir / "server.key").write_text("fake-plugin-key")

        config = MCPStackConfig.model_validate(
            {
                "deployment": {"type": "compose"},
                "gateway": {
                    "image": "mcpgateway:latest",
                    "port": 4444,
                    "host_port": 4444,
                    "mtls_enabled": True,
                },
                "plugins": [
                    {
                        "name": "SecurePlugin",
                        "image": "secure:v1",
                        "port": 8000,
                        "mtls_enabled": True,
                    }
                ],
            }
        )

        with patch("mcpgateway.tools.builder.common.Path.cwd", return_value=tmp_path):
            generate_compose_manifests(config, output_dir)

        # Parse and validate
        compose_file = output_dir / "docker-compose.yaml"
        with open(compose_file) as f:
            compose_data = yaml.safe_load(f)

        # Verify gateway has certificate volumes
        gateway = compose_data["services"]["mcpgateway"]
        assert "volumes" in gateway
        # Should have volume mounts for certificates
        volumes = gateway["volumes"]
        assert any("certs" in str(v) or "ca.crt" in str(v) for v in volumes)

        # Verify plugin has certificate volumes
        plugin = compose_data["services"]["secureplugin"]
        assert "volumes" in plugin

    def test_generate_compose_with_env_files(self, tmp_path):
        """Test generating Docker Compose manifest with environment files."""
        output_dir = tmp_path / "manifests"
        output_dir.mkdir()

        # Create env files
        deploy_dir = tmp_path / "deploy"
        env_dir = deploy_dir / "env"
        env_dir.mkdir(parents=True)
        (env_dir / ".env.gateway").write_text("GATEWAY_VAR=value1\n")
        (env_dir / ".env.TestPlugin").write_text("PLUGIN_VAR=value2\n")

        config = MCPStackConfig.model_validate(
            {
                "deployment": {"type": "compose"},
                "gateway": {
                    "image": "mcpgateway:latest",
                    "port": 4444,
                    "mtls_enabled": False,
                },
                "plugins": [
                    {
                        "name": "TestPlugin",
                        "image": "test:v1",
                        "port": 8000,
                        "mtls_enabled": False,
                    }
                ],
            }
        )

        with patch("mcpgateway.tools.builder.common.get_deploy_dir", return_value=deploy_dir):
            with patch("mcpgateway.tools.builder.common.Path.cwd", return_value=tmp_path):
                generate_compose_manifests(config, output_dir)

        # Parse and validate
        compose_file = output_dir / "docker-compose.yaml"
        with open(compose_file) as f:
            compose_data = yaml.safe_load(f)

        # Verify env_file is set
        gateway = compose_data["services"]["mcpgateway"]
        assert "env_file" in gateway

        plugin = compose_data["services"]["testplugin"]
        assert "env_file" in plugin

    def test_generate_compose_with_infrastructure(self, tmp_path):
        """Test generating Docker Compose manifest with PostgreSQL and Redis.

        Note: Currently the template uses hardcoded infrastructure images/config.
        Infrastructure customization is not yet implemented for Docker Compose.
        """
        output_dir = tmp_path / "manifests"
        output_dir.mkdir()

        config = MCPStackConfig.model_validate(
            {
                "deployment": {"type": "compose"},
                "gateway": {
                    "image": "mcpgateway:latest",
                    "port": 4444,
                    "mtls_enabled": False,
                },
                "plugins": [],
                "infrastructure": {
                    "postgres": {
                        "enabled": True,
                        "image": "postgres:17",
                        "database": "mcpdb",
                        "user": "mcpuser",
                        "password": "secret123",
                    },
                    "redis": {
                        "enabled": True,
                        "image": "redis:7-bookworm",
                    },
                },
            }
        )

        with patch("mcpgateway.tools.builder.common.Path.cwd", return_value=tmp_path):
            generate_compose_manifests(config, output_dir)

        # Parse and validate
        compose_file = output_dir / "docker-compose.yaml"
        with open(compose_file) as f:
            compose_data = yaml.safe_load(f)

        # Verify PostgreSQL service exists
        # Note: Template uses hardcoded "postgres:17" and "mcp" database
        assert "postgres" in compose_data["services"]
        postgres = compose_data["services"]["postgres"]
        assert postgres["image"] == "postgres:17"  # Hardcoded in template
        assert "environment" in postgres

        # Verify database name is "mcp" (hardcoded default, not "mcpdb" from config)
        env = postgres["environment"]
        if isinstance(env, list):
            assert any("POSTGRES_DB=mcp" in str(e) for e in env)
        else:
            assert env["POSTGRES_DB"] == "mcp"

        # Verify Redis service exists
        # Note: Template uses hardcoded "redis:latest"
        assert "redis" in compose_data["services"]
        redis = compose_data["services"]["redis"]
        assert redis["image"] == "redis:latest"  # Hardcoded in template

        # Verify gateway has database environment variables
        gateway = compose_data["services"]["mcpgateway"]
        assert "environment" in gateway
        env = gateway["environment"]

        # Should have DATABASE_URL with default values (uses postgresql+psycopg:// for psycopg3)
        if isinstance(env, list):
            db_url = next((e for e in env if "DATABASE_URL" in str(e)), None)
        else:
            db_url = env.get("DATABASE_URL")
        assert db_url is not None
        assert "postgresql+psycopg://" in str(db_url)
