"""
Integration tests for Docker build process.

Tests validate that the Dockerfile builds successfully and produces
a functional container with required utilities for healthchecks.
"""

import json
import os
import re
import subprocess
import tempfile
import pytest


@pytest.fixture(scope="module")
def docker_image(project_root):
    """
    Build Docker image once for all tests in this module.

    This fixture builds the image once and reuses it across all tests,
    significantly improving test performance (30s build vs 3.5min for 7 builds).
    """
    image_name = "mca-otel-collector:test"

    # Build the image
    result = subprocess.run(
        ["docker", "build", "-t", image_name, "-f", "mca/Dockerfile", "."],
        cwd=str(project_root),
        capture_output=True,
        text=True,
        timeout=120,
    )

    if result.returncode != 0:
        pytest.fail(
            f"Failed to build Docker image:\nSTDOUT: {result.stdout}\nSTDERR: {result.stderr}"
        )

    yield image_name

    # Cleanup after all tests complete
    # First check if any containers are using the image (continue even if this fails)
    try:
        containers_result = subprocess.run(
            [
                "docker",
                "ps",
                "-a",
                "--filter",
                f"ancestor={image_name}",
                "--format",
                "{{.ID}}",
            ],
            capture_output=True,
            text=True,
            timeout=10,
        )

        if containers_result.returncode == 0 and containers_result.stdout.strip():
            container_ids = containers_result.stdout.strip().split("\n")
            for container_id in container_ids:
                try:
                    subprocess.run(
                        ["docker", "rm", "-f", container_id],
                        capture_output=True,
                        timeout=10,
                    )
                except Exception as e:
                    print(f"WARNING: Failed to remove container {container_id}: {e}")
    except Exception as e:
        print(f"WARNING: Failed to query containers for image {image_name}: {e}")

    # Now remove the image (continue cleanup even if container removal failed)
    try:
        cleanup_result = subprocess.run(
            ["docker", "rmi", "-f", image_name],
            capture_output=True,
            text=True,
            timeout=10,
        )

        # Log warning if cleanup fails (indicates resource leak)
        if cleanup_result.returncode != 0:
            import warnings

            warnings.warn(
                f"Failed to cleanup image {image_name}. "
                f"Future test runs may use stale image.\n"
                f"Error: {cleanup_result.stderr}"
            )
    except Exception as e:
        import warnings

        warnings.warn(f"Failed to remove image {image_name}: {e}")


class TestDockerBuild:
    """Test suite for Docker image building."""

    def test_dockerfile_builds_successfully(self, docker_image):
        """
        Test that Docker build completes without errors.

        Validates:
        - Dockerfile syntax is valid
        - Build process completes successfully
        - Image is tagged correctly
        """
        # Verify image exists (fixture already built it)
        result = subprocess.run(
            ["docker", "images", docker_image, "--format", "{{.Repository}}:{{.Tag}}"],
            capture_output=True,
            text=True,
        )

        assert result.returncode == 0, "Failed to inspect image"
        assert docker_image in result.stdout, f"Image {docker_image} not found in Docker images"

    def test_image_size_is_reasonable(self, docker_image):
        """
        Test that the built image is not excessively large.

        Validates:
        - Image size is under 150MB (Alpine + OTel binary expected)
        - Multi-stage build is working correctly
        """
        # Get image size in bytes for accurate comparison
        result = subprocess.run(
            ["docker", "inspect", "--format", "{{.Size}}", docker_image],
            capture_output=True,
            text=True,
        )

        assert result.returncode == 0, "Failed to inspect image"

        # Size is in bytes as integer
        size_bytes = int(result.stdout.strip())
        size_mb = size_bytes / (1024 * 1024)

        # 150MB threshold
        threshold_mb = 150

        assert size_mb < threshold_mb, (
            f"Image size ({size_mb:.2f}MB) exceeds {threshold_mb}MB threshold. "
            f"Multi-stage build may not be working correctly."
        )

    def test_image_contains_wget(self, docker_image):
        """
        Test that the image contains wget utility.

        Validates:
        - Alpine base includes wget
        - Healthcheck can use wget command
        """
        # Check if wget is available
        result = subprocess.run(
            ["docker", "run", "--rm", docker_image, "which", "wget"],
            capture_output=True,
            text=True,
            timeout=10,
        )

        assert (
            result.returncode == 0
        ), "wget not found in container. Healthcheck will fail without wget."
        assert "/usr/bin/wget" in result.stdout, f"Unexpected wget path: {result.stdout}"

    def test_image_contains_ca_certificates(self, docker_image):
        """
        Test that the image contains CA certificates.

        Validates:
        - ca-certificates package is installed
        - HTTPS connections to GCP will work
        """
        # Check if ca-certificates directory exists
        result = subprocess.run(
            ["docker", "run", "--rm", docker_image, "ls", "/etc/ssl/certs"],
            capture_output=True,
            text=True,
            timeout=10,
        )

        assert result.returncode == 0, "CA certificates not found. HTTPS export to GCP will fail."
        assert len(result.stdout.strip()) > 0, "CA certificates directory is empty"

    def test_otelcol_binary_is_present(self, docker_image):
        """
        Test that otelcol-contrib binary is present and executable.

        Validates:
        - Binary copied from official image
        - Binary has execute permissions
        """
        # Check if binary exists and is executable
        result = subprocess.run(
            ["docker", "run", "--rm", docker_image, "which", "otelcol-contrib"],
            capture_output=True,
            text=True,
            timeout=10,
        )

        assert result.returncode == 0, "otelcol-contrib binary not found in PATH"
        assert "/otelcol-contrib" in result.stdout, f"Unexpected binary path: {result.stdout}"

    def test_ports_are_exposed(self, docker_image):
        """
        Test that required ports are exposed in the image.

        Validates:
        - Port 4318 (OTLP HTTP) is exposed
        - Port 13133 (healthcheck) is exposed
        """
        # Inspect image to check exposed ports
        result = subprocess.run(["docker", "inspect", docker_image], capture_output=True, text=True)

        assert result.returncode == 0, "Failed to inspect image"

        # Parse JSON output
        image_data = json.loads(result.stdout)

        exposed_ports = image_data[0]["Config"].get("ExposedPorts", {})

        assert "4318/tcp" in exposed_ports, "Port 4318 (OTLP HTTP) not exposed"
        assert "13133/tcp" in exposed_ports, "Port 13133 (healthcheck) not exposed"

    def test_binary_architecture_matches_host(self, docker_image):
        """
        Test that otelcol-contrib binary architecture matches host platform.

        Validates:
        - Binary is correct architecture (amd64/arm64)
        - Binary can execute on host architecture
        - Prevents exec format errors
        """
        # Get binary file info
        result = subprocess.run(
            [
                "docker",
                "run",
                "--rm",
                docker_image,
                "file",
                "/usr/local/bin/otelcol-contrib",
            ],
            capture_output=True,
            text=True,
            timeout=10,
        )

        assert result.returncode == 0, "Failed to check binary file info"

        file_info = result.stdout.lower()

        # Verify it's an ELF executable
        assert "elf" in file_info, f"Binary is not ELF format: {result.stdout}"

        # Check for valid architectures (x86-64 or aarch64/ARM)
        is_x86_64 = "x86-64" in file_info or "x86_64" in file_info
        is_arm64 = "aarch64" in file_info or "arm64" in file_info

        assert (
            is_x86_64 or is_arm64
        ), f"Binary architecture not recognized as x86-64 or arm64: {result.stdout}"

        # Verify binary is executable and check version
        exec_result = subprocess.run(
            ["docker", "run", "--rm", docker_image, "otelcol-contrib", "--version"],
            capture_output=True,
            text=True,
            timeout=10,
        )

        assert exec_result.returncode == 0, f"Binary failed to execute: {exec_result.stderr}"

        # Parse and validate version output
        version_output = exec_result.stdout
        assert (
            "0.115.1" in version_output
        ), f"Expected OTel Collector version 0.115.1, got: {version_output}"

    def test_container_fails_with_invalid_config(self, docker_image, project_root):
        """
        Test that container fails fast with invalid configuration.

        Validates:
        - Container exits with error on invalid config
        - Error message indicates config problem
        - Prevents silent failures
        """
        # Create invalid config (valid YAML but invalid OTel semantics)
        invalid_config = """
receivers:
  nonexistent_receiver:
    endpoint: 0.0.0.0:4318

processors:
  batch:

exporters:
  debug:

service:
  pipelines:
    traces:
      receivers: [nonexistent_receiver]
      processors: [batch]
      exporters: [debug]
"""

        # Write invalid config to temp file
        with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
            f.write(invalid_config)
            invalid_config_path = f.name

        try:
            # Try to run container with invalid config
            result = subprocess.run(
                [
                    "docker",
                    "run",
                    "--rm",
                    "-v",
                    f"{invalid_config_path}:/etc/otelcol-contrib/config.yaml:ro",
                    docker_image,
                ],
                capture_output=True,
                text=True,
                timeout=15,
            )

            # Container should exit with error
            assert (
                result.returncode != 0
            ), "Container did not fail with invalid config. Expected non-zero exit code."

            # Error message should indicate config issue
            error_output = result.stdout + result.stderr
            assert (
                "error" in error_output.lower()
                or "failed" in error_output.lower()
                or "invalid" in error_output.lower()
            ), f"Error message does not clearly indicate config problem:\n{error_output}"

        finally:
            if os.path.exists(invalid_config_path):
                os.unlink(invalid_config_path)

    def test_dockerfile_uses_pinned_versions_with_digests(self, project_root):
        """
        Test that Dockerfile uses pinned versions with SHA256 digests.

        Validates:
        - OTel Collector has specific version + digest
        - Alpine has specific version + digest
        - Supply-chain integrity requirements met
        """
        dockerfile_path = project_root / "mca" / "Dockerfile"

        with open(dockerfile_path, "r") as f:
            dockerfile_content = f.read()

        # Check OTel Collector version pinning
        assert (
            "otel/opentelemetry-collector-contrib:0.115.1@sha256:" in dockerfile_content
        ), "OTel Collector is not pinned to version 0.115.1 with SHA256 digest"

        # Check Alpine version pinning
        assert (
            "alpine:3.19.1@sha256:" in dockerfile_content
        ), "Alpine base image is not pinned to version 3.19.1 with SHA256 digest"

        # Verify digest format (64 hex chars)
        digests = re.findall(r"@sha256:([a-f0-9]{64})", dockerfile_content)
        assert len(digests) >= 2, f"Expected at least 2 SHA256 digests, found {len(digests)}"

        # Verify expected digests
        expected_otel_digest = "355f747b76c723036055d027e95326fbba76c345c190c17389b2451ad15ffd16"
        expected_alpine_digest = "6457d53fb065d6f250e1504b9bc42d5b6c65941d57532c072d929dd0628977d0"

        assert (
            expected_otel_digest in dockerfile_content
        ), f"OTel Collector digest mismatch. Expected {expected_otel_digest}"
        assert (
            expected_alpine_digest in dockerfile_content
        ), f"Alpine digest mismatch. Expected {expected_alpine_digest}"
