#!/usr/bin/env python3
"""
test_vllm_sr_serve.py - Tests for 'vllm-sr serve' command.

This test validates the serve command:
- Container startup from config.yaml
- Health check completion
- Image pull policy behavior (always, ifnotpresent, never)
- Environment variable passing (HF_TOKEN, etc.)
- Volume mounting for config and models
- Port mapping from listeners

Signed-off-by: vLLM-SR Team
"""

import os
import unittest

from cli_test_base import CLITestBase


class TestVllmSRServe(CLITestBase):
    """Tests for the vllm-sr serve command."""

    # Extended timeout for serve tests (model loading can be slow)
    SERVE_TIMEOUT = 600

    def tearDown(self):
        """Clean up after each serve test."""
        # Always stop container after serve tests
        self.run_cli(["stop"], timeout=30)
        self._cleanup_container()
        super().tearDown()

    def _create_minimal_config(self, port: int = 8888) -> str:
        """Create a lean active config.yaml for testing."""
        config_content = f"""version: v0.3

listeners:
  - name: "test-listener"
    address: "0.0.0.0"
    port: {port}
    timeout: "60s"

providers:
  default_model: "test-model"
  models:
    - name: "test-model"
      provider_model_id: "test-model"
      backend_refs:
        - name: "test_endpoint"
          weight: 1
          endpoint: "host.docker.internal:8000"
          protocol: "http"

routing:
  modelCards:
    - name: "test-model"
  decisions:
    - name: "default_route"
      description: "Default test route"
      priority: 100
      rules:
        operator: "AND"
        conditions: []
      modelRefs:
        - model: "test-model"
          use_reasoning: false
"""
        config_path = os.path.join(self.test_dir, "config.yaml")
        with open(config_path, "w") as f:
            f.write(config_content)
        return config_path

    def test_serve_bootstraps_missing_config_file(self):
        """Test that serve bootstraps setup mode when config.yaml is missing."""
        self.print_test_header(
            "Serve Bootstraps Missing Config File",
            "Validates that serve creates bootstrap setup files instead of requiring init",
        )

        # Run serve without creating config.yaml. Use a fake image and never-pull so the
        # command fails after bootstrap instead of waiting on a real container image.
        return_code, stdout, stderr = self.run_cli(
            [
                "serve",
                "--image",
                "nonexistent-image:bootstrap",
                "--image-pull-policy",
                "never",
            ],
            timeout=30,
        )

        # The command may still fail because the image does not exist, but it should no
        # longer fail because config.yaml is missing.
        output = (stdout + stderr).lower()
        self.assertTrue(
            os.path.exists(os.path.join(self.test_dir, "config.yaml")),
            "serve should create a bootstrap config.yaml",
        )
        self.assertTrue(
            os.path.isdir(os.path.join(self.test_dir, ".vllm-sr")),
            "serve should create a bootstrap .vllm-sr directory",
        )
        self.assertNotIn(
            "create a config file before running serve",
            output,
            "serve should no longer require manual pre-bootstrap setup before startup",
        )
        self.assertIn(
            "bootstrap",
            output,
            f"Expected bootstrap messaging. Got: {output[:300]}",
        )
        self.assertNotEqual(
            return_code, 0, "fake image should still cause a startup failure"
        )

        self.print_test_result(
            True, "serve bootstraps setup mode when config is missing"
        )

    def test_serve_with_custom_config_path(self):
        """Test that serve accepts --config flag for custom config path."""
        self.print_test_header(
            "Serve With Custom Config Path",
            "Validates that --config flag allows custom config file location",
        )

        # Create config in a subdirectory
        subdir = os.path.join(self.test_dir, "configs")
        os.makedirs(subdir)
        config_path = os.path.join(subdir, "custom-config.yaml")

        config_content = """version: v0.3
listeners:
  - name: "test"
    address: "0.0.0.0"
    port: 8888
    timeout: "60s"
providers:
  default_model: "test-model"
  models:
    - name: "test-model"
      provider_model_id: "test-model"
      backend_refs:
        - name: "primary"
          endpoint: "host.docker.internal:8000"
          protocol: "http"
routing:
  modelCards:
    - name: "test-model"
  decisions:
    - name: "default-route"
      description: "Catch-all route"
      priority: 100
      rules:
        operator: "AND"
        conditions: []
      modelRefs:
        - model: "test-model"
          use_reasoning: false
"""
        with open(config_path, "w") as f:
            f.write(config_content)

        # Run serve with --config pointing to custom path
        # Use --image-pull-policy=never to avoid pulling and fail fast if image missing
        _return_code, stdout, stderr = self.run_cli(
            ["serve", "--config", config_path, "--image-pull-policy", "never"],
            timeout=30,
        )

        # The command might fail due to missing image, but should recognize the config
        output = (stdout + stderr).lower()

        # Should NOT complain about config not found
        self.assertNotIn(
            "config file not found",
            output,
            "Should accept custom config path",
        )

        self.print_test_result(True, "serve accepts custom config path")

    def test_serve_image_pull_policy_never(self):
        """Test --image-pull-policy=never fails when image doesn't exist locally."""
        self.print_test_header(
            "Image Pull Policy: never",
            "Validates that never policy fails when image is not present locally",
        )

        # Create config
        self._create_minimal_config()

        # Use a non-existent image with never policy
        return_code, stdout, stderr = self.run_cli(
            [
                "serve",
                "--image",
                "nonexistent-image:12345",
                "--image-pull-policy",
                "never",
            ],
            timeout=30,
        )

        # Should fail
        self.assertNotEqual(return_code, 0, "Should fail with non-existent image")

        # When the daemon is reachable, serve should fail on the missing image itself.
        # When the daemon is down, the command may fail earlier while preparing runtime
        # resources, but it should still surface a container-runtime issue rather than a
        # config/flag parsing error.
        output = (stdout + stderr).lower()
        if self.container_runtime_accessible():
            self.assertTrue(
                "not found" in output or "never" in output or "image" in output,
                f"Should mention image issue. Got: {output[:300]}",
            )
        else:
            self.assertTrue(
                "daemon" in output or "docker" in output,
                f"Should mention container runtime issue. Got: {output[:300]}",
            )

        self.print_test_result(True, "never policy correctly fails for missing image")

    def test_serve_image_pull_policy_ifnotpresent(self):
        """Test --image-pull-policy=ifnotpresent behavior."""
        self.print_test_header(
            "Image Pull Policy: ifnotpresent",
            "Validates ifnotpresent policy only pulls when image is missing",
        )

        # Create config
        self._create_minimal_config()

        # This test documents the expected behavior without requiring actual pull
        # In CI, the image should already be built and loaded

        # Check if the default image exists
        default_image = "ghcr.io/vllm-project/semantic-router/vllm-sr:latest"
        image_exists = self.image_exists(default_image)

        print(f"Default image exists locally: {image_exists}")

        # Run with ifnotpresent - should not fail immediately for image reasons
        # (though may fail later for other reasons like model loading)
        _, _, _ = self.run_cli(
            ["serve", "--image-pull-policy", "ifnotpresent"],
            timeout=60,
        )

        # Document behavior
        if image_exists:
            print("  Image exists - should skip pull")
            # Should see "Image exists locally" or similar message
        else:
            print("  Image missing - would attempt pull")

        self.print_test_result(True, "ifnotpresent policy documented")

    def test_serve_image_pull_policy_always(self):
        """Test --image-pull-policy=always behavior."""
        self.print_test_header(
            "Image Pull Policy: always",
            "Validates always policy attempts to pull even when image exists locally",
        )

        # Create config
        self._create_minimal_config()

        # Test that the 'always' policy flag is recognized
        # Note: We can't fully test the pull behavior without network access
        # but we can verify the flag is accepted and behavior is documented

        _, stdout, stderr = self.run_cli(
            ["serve", "--image-pull-policy", "always"],
            timeout=30,
        )

        output = (stdout + stderr).lower()

        # The 'always' flag should be recognized (not "unknown flag" error)
        unknown_flag = "unknown" in output and "always" in output
        self.assertFalse(unknown_flag, "always policy should be recognized")

        # Document expected behavior
        print("  Expected behavior with 'always' policy:")
        print("    - Always attempts to pull latest image from registry")
        print("    - Even if image exists locally, pull to check for updates")
        print("    - Requires network access to registry")

        self.print_test_result(True, "always policy documented")

    def test_serve_passes_hf_token_env_var(self):
        """Test that HF_TOKEN environment variable is passed to container."""
        self.print_test_header(
            "Environment Variable Passing: HF_TOKEN",
            "Validates that HF_TOKEN is passed to the container via -e flag",
        )

        # Create config
        self._create_minimal_config()

        # Set HF_TOKEN in environment
        test_token = "hf_test_token_12345"

        # Run serve with HF_TOKEN - use verbose/debug to see docker command
        _, stdout, stderr = self.run_cli(
            ["serve", "--image-pull-policy", "never"],
            env={"HF_TOKEN": test_token},
            timeout=30,
        )

        output = (stdout + stderr).lower()

        # Verify the docker command includes -e for environment variable
        # The CLI should pass HF_TOKEN to docker with -e HF_TOKEN=...
        env_passed = False

        # Check for evidence that env var is being passed to docker
        # Look for: -e HF_TOKEN, --env HF_TOKEN, or environment variable mentions
        if "-e hf_token" in output or "--env hf_token" in output:
            print("  ✓ HF_TOKEN passed to docker via -e flag")
            env_passed = True
        elif "hf_token" in output and ("environment" in output or "env" in output):
            print("  ✓ HF_TOKEN environment variable handling detected")
            env_passed = True
        elif "hf_token" in output:
            # At minimum, CLI acknowledges HF_TOKEN
            print("  ✓ HF_TOKEN acknowledged by CLI")
            env_passed = True

        # Verify token value is NOT exposed in plain text (security)
        if test_token not in (stdout + stderr):
            print("  ✓ Token value properly masked (not in plain text)")
        else:
            print("  ⚠ Warning: Token value visible in output")

        self.print_test_result(env_passed, "HF_TOKEN environment variable passing")

    def test_serve_port_mapping(self):
        """Test that listener ports are correctly mapped."""
        self.print_test_header(
            "Port Mapping from Config",
            "Validates that listener ports from config.yaml are mapped correctly",
        )

        # Create config with specific port
        test_port = 8999
        self._create_minimal_config(port=test_port)

        # Run serve (will likely fail due to missing image, but we can check the command built)
        _, stdout, stderr = self.run_cli(
            ["serve", "--image-pull-policy", "never"],
            timeout=30,
        )

        output = stdout + stderr

        # Check that the port is mentioned in output
        if str(test_port) in output:
            print(f"  ✓ Port {test_port} mentioned in output")

        self.print_test_result(True, "Port mapping behavior validated")

    def test_serve_readonly_flag(self):
        """Test that serve accepts --readonly."""
        self.print_test_header(
            "Readonly Dashboard Flag",
            "Validates that --readonly flag is recognized",
        )

        # Create config
        self._create_minimal_config()

        # Run serve with --readonly flag
        _, stdout, stderr = self.run_cli(
            ["serve", "--readonly", "--image-pull-policy", "never"],
            timeout=30,
        )

        output = (stdout + stderr).lower()

        # The flag should be recognized (not "unknown flag" error)
        self.assertNotIn(
            "no such option: --readonly", output, "--readonly flag should be recognized"
        )
        self.assertNotIn(
            "did you mean --readonly",
            output,
            "--readonly should be the canonical serve flag",
        )

        self.print_test_result(True, "--readonly flag is recognized")

    def test_serve_mounts_config_file(self):
        """Test that config file is mounted into container."""
        self.print_test_header(
            "Config File Mounting",
            "Validates that config.yaml is mounted into the container",
        )

        # Create config with identifiable content
        self._create_minimal_config()

        # The actual mount verification would require the container to start
        # This test documents expected behavior
        print("  Expected: config.yaml mounted at /app/config.yaml")
        print("  Expected: .vllm-sr/ mounted at /app/.vllm-sr/ if it exists")
        print("  Expected: models/ mounted at /app/models/ for model caching")

        self.print_test_result(True, "Volume mounting expectations documented")

    def test_serve_creates_models_directory(self):
        """Test that serve creates models directory for model caching."""
        self.print_test_header(
            "Models Directory Creation",
            "Validates that serve creates models/ directory for caching",
        )

        # Create config
        self._create_minimal_config()

        # Models directory should not exist yet
        models_dir = os.path.join(self.test_dir, "models")
        self.assertFalse(
            os.path.exists(models_dir),
            "models/ should not exist before serve",
        )

        # Run serve (even if it fails, it should create the directory)
        _, _, _ = self.run_cli(
            ["serve", "--image-pull-policy", "never"],
            timeout=30,
        )

        # Check if models directory was created
        # Note: This depends on how early in the process the directory is created
        if os.path.exists(models_dir):
            print("  ✓ models/ directory created")
            self.print_test_result(True, "models/ directory created for caching")
        else:
            print(
                "  ⚠ models/ directory not created (may require successful container start)"
            )
            self.print_test_result(
                True, "Test completed - directory creation timing may vary"
            )


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