"""
Integration tests for Docker Compose orchestration.

Tests validate that docker-compose.yml correctly orchestrates
the OTel Collector with proper networking, healthchecks, and volume mounts.
"""

import json
import os
import shutil
import subprocess
import tempfile
import time
import pytest


class TestDockerCompose:
    """Test suite for Docker Compose orchestration."""

    @pytest.fixture(scope="class", autouse=True)
    def compose_lifecycle(self, project_root):
        """
        Start and stop docker-compose stack for tests.

        Manages the lifecycle of the compose stack to ensure
        clean state for testing.
        """
        # Start compose stack
        result = subprocess.run(
            ["docker-compose", "up", "-d", "otel-collector"],
            cwd=str(project_root),
            capture_output=True,
            text=True,
        )

        if result.returncode != 0:
            pytest.fail(
                f"Failed to start compose stack:\nSTDOUT: {result.stdout}\nSTDERR: {result.stderr}"
            )

        # Poll for container to be running (max 60s with 2s intervals)
        max_wait = 60
        start_time = time.time()
        container_running = False

        while time.time() - start_time < max_wait:
            ps_result = subprocess.run(
                [
                    "docker",
                    "ps",
                    "--filter",
                    "name=mca-otel-collector",
                    "--format",
                    "{{.Status}}",
                ],
                capture_output=True,
                text=True,
            )

            if ps_result.returncode == 0 and "Up" in ps_result.stdout:
                container_running = True
                break

            time.sleep(2)

        if not container_running:
            pytest.fail("Container failed to start within 60 seconds")

        yield

        # Cleanup after tests with full teardown
        subprocess.run(
            ["docker-compose", "down", "--volumes", "--remove-orphans"],
            cwd=str(project_root),
            capture_output=True,
        )

    def test_otel_collector_service_starts(self):
        """
        Test that otel-collector service starts successfully.

        Validates:
        - Container is running
        - Container name matches expected
        """
        result = subprocess.run(
            [
                "docker",
                "ps",
                "--filter",
                "name=mca-otel-collector",
                "--format",
                "{{.Names}}",
            ],
            capture_output=True,
            text=True,
        )

        assert result.returncode == 0, "Failed to list Docker containers"
        assert "mca-otel-collector" in result.stdout, "otel-collector container is not running"

    def test_otel_collector_is_healthy(self):
        """
        Test that otel-collector reports healthy status.

        Validates:
        - Healthcheck is configured
        - Healthcheck passes successfully
        - Container reaches healthy state
        """
        # Wait for healthcheck to stabilize (up to 30 seconds)
        max_wait = 30
        start_time = time.time()

        while time.time() - start_time < max_wait:
            result = subprocess.run(
                [
                    "docker",
                    "inspect",
                    "--format",
                    "{{.State.Health.Status}}",
                    "mca-otel-collector",
                ],
                capture_output=True,
                text=True,
            )

            if result.returncode == 0:
                status = result.stdout.strip()
                if status == "healthy":
                    break

            time.sleep(2)

        # Final check
        result = subprocess.run(
            [
                "docker",
                "inspect",
                "--format",
                "{{.State.Health.Status}}",
                "mca-otel-collector",
            ],
            capture_output=True,
            text=True,
        )

        assert result.returncode == 0, "Failed to inspect container health"

        health_status = result.stdout.strip()
        assert health_status == "healthy", f"Container is not healthy. Status: {health_status}"

    def test_ports_are_mapped_correctly(self):
        """
        Test that required ports are mapped from host to container.

        Validates:
        - Port 4318 is accessible (OTLP HTTP)
        - Port 13133 is accessible (healthcheck)
        """
        result = subprocess.run(
            ["docker", "port", "mca-otel-collector"], capture_output=True, text=True
        )

        assert result.returncode == 0, "Failed to inspect container ports"

        output = result.stdout

        assert "4318/tcp" in output, "Port 4318 (OTLP HTTP) not mapped"
        assert "13133/tcp" in output, "Port 13133 (healthcheck) not mapped"

        # Verify port mapping is to correct host ports
        assert (
            "0.0.0.0:4318" in output or ":::4318" in output
        ), "Port 4318 not mapped to host port 4318"
        assert (
            "0.0.0.0:13133" in output or ":::13133" in output
        ), "Port 13133 not mapped to host port 13133"

    def test_config_volume_is_mounted(self):
        """
        Test that config file is mounted as read-only volume.

        Validates:
        - Config file exists in container
        - Mount is read-only
        """
        # Check if config file exists in container
        result = subprocess.run(
            [
                "docker",
                "exec",
                "mca-otel-collector",
                "cat",
                "/etc/otelcol-contrib/config.yaml",
            ],
            capture_output=True,
            text=True,
        )

        assert result.returncode == 0, "Config file not accessible in container"
        assert len(result.stdout) > 0, "Config file is empty"
        assert "receivers:" in result.stdout, "Config file content is invalid"

        # Check mount is read-only
        inspect_result = subprocess.run(
            ["docker", "inspect", "mca-otel-collector"], capture_output=True, text=True
        )

        assert inspect_result.returncode == 0, "Failed to inspect container"

        # Parse for mount info
        container_data = json.loads(inspect_result.stdout)
        mounts = container_data[0]["Mounts"]

        config_mount = None
        for mount in mounts:
            if mount["Destination"] == "/etc/otelcol-contrib/config.yaml":
                config_mount = mount
                break

        assert config_mount is not None, "Config volume mount not found"
        assert config_mount["RW"] is False, "Config mount is not read-only"

        # Actually verify filesystem rejects writes
        write_test = subprocess.run(
            [
                "docker",
                "exec",
                "mca-otel-collector",
                "sh",
                "-c",
                "echo test >> /etc/otelcol-contrib/config.yaml 2>&1",
            ],
            capture_output=True,
            text=True,
        )

        # Should fail with permission denied or read-only filesystem error
        assert write_test.returncode != 0, "Config mount allowed write (should be read-only)"
        assert (
            "Read-only file system" in write_test.stdout
            or "Read-only file system" in write_test.stderr
            or "Permission denied" in write_test.stdout
            or "Permission denied" in write_test.stderr
        ), f"Expected read-only error, got: {write_test.stdout} {write_test.stderr}"

    def test_mca_network_exists(self, project_root):
        """
        Test that mca-network bridge network is created.

        Validates:
        - Network is created by docker-compose
        - Network driver is bridge
        """
        # Query actual network from container instead of guessing
        network_result = subprocess.run(
            [
                "docker",
                "inspect",
                "--format",
                "{{range $net, $conf := .NetworkSettings.Networks}}{{$net}}{{end}}",
                "mca-otel-collector",
            ],
            capture_output=True,
            text=True,
        )

        assert network_result.returncode == 0, "Failed to inspect container networks"
        actual_network = network_result.stdout.strip()

        # Verify network exists in Docker
        verify_result = subprocess.run(
            ["docker", "network", "inspect", actual_network],
            capture_output=True,
            text=True,
        )

        assert verify_result.returncode == 0, f"Network {actual_network} not found in Docker"

        # Verify it's a bridge network
        network_data = json.loads(verify_result.stdout)
        assert (
            network_data[0]["Driver"] == "bridge"
        ), f"Network {actual_network} is not a bridge network"

    def test_otel_collector_on_mca_network(self):
        """
        Test that otel-collector is connected to compose network.

        Validates:
        - Container is on a network
        - Network is accessible for inter-container communication
        """
        result = subprocess.run(
            [
                "docker",
                "inspect",
                "--format",
                "{{json .NetworkSettings.Networks}}",
                "mca-otel-collector",
            ],
            capture_output=True,
            text=True,
        )

        assert result.returncode == 0, "Failed to inspect container networks"

        # Parse network data
        networks = json.loads(result.stdout)

        # Container should be on at least one network
        assert len(networks) > 0, "Container not connected to any network"

        # At least one network should have a valid IP address
        has_ip = any(net_conf.get("IPAddress") for net_conf in networks.values())
        assert has_ip, "Container has no IP address on any network"

    def test_collector_accepts_otlp_requests(self):
        """
        Test that collector can receive OTLP requests on port 4318.

        Validates:
        - Port 4318 is listening
        - OTLP endpoint processes valid payloads correctly
        """
        # Send minimal valid OTLP/HTTP request to v1/traces endpoint
        # Empty JSON array is valid OTLP format
        result = subprocess.run(
            [
                "curl",
                "-X",
                "POST",
                "-H",
                "Content-Type: application/json",
                "-d",
                '{"resourceSpans": []}',
                "-s",
                "-o",
                "/dev/null",
                "-w",
                "%{http_code}",
                "http://localhost:4318/v1/traces",
            ],
            capture_output=True,
            text=True,
            timeout=5,
        )

        assert result.returncode == 0, "Failed to connect to collector port 4318"

        # Only accept 200/202 which indicate successful OTLP processing
        # 404 = endpoint doesn't exist, 405 = wrong method, both indicate broken collector
        http_code = result.stdout.strip()
        assert http_code in ["200", "202"], (
            f"OTLP endpoint returned error status {http_code}. "
            f"Expected 200/202 for successful processing."
        )

    def test_healthcheck_endpoint_responds(self):
        """
        Test that healthcheck endpoint responds correctly.

        Validates:
        - Port 13133 is accessible
        - Health endpoint returns successful response
        """
        # Poll for healthy status instead of sleeping
        max_wait = 30
        start_time = time.time()
        endpoint_ready = False

        while time.time() - start_time < max_wait:
            try:
                result = subprocess.run(
                    [
                        "curl",
                        "-s",
                        "-o",
                        "/dev/null",
                        "-w",
                        "%{http_code}",
                        "http://localhost:13133/",
                    ],
                    capture_output=True,
                    text=True,
                    timeout=5,
                )
                if result.returncode == 0 and result.stdout.strip() == "200":
                    endpoint_ready = True
                    break
            except Exception:
                pass
            time.sleep(2)

        assert endpoint_ready, "Healthcheck endpoint did not respond within 30 seconds"

    def test_collector_processes_otlp_data(self):
        """
        Test that collector actually processes OTLP telemetry data.

        Validates:
        - Collector parses OTLP payload
        - Pipeline processes data successfully
        - Data flows through receivers -> processors -> exporters
        """
        # Send valid OTLP trace data
        otlp_payload = {
            "resourceSpans": [
                {
                    "resource": {
                        "attributes": [
                            {
                                "key": "service.name",
                                "value": {"stringValue": "test-service"},
                            }
                        ]
                    },
                    "scopeSpans": [
                        {
                            "spans": [
                                {
                                    "traceId": "5b8efff798038103d269b633813fc60c",
                                    "spanId": "eee19b7ec3c1b173",
                                    "name": "test-span",
                                    "kind": 1,
                                    "startTimeUnixNano": "1544712660000000000",
                                    "endTimeUnixNano": "1544712661000000000",
                                }
                            ]
                        }
                    ],
                }
            ]
        }

        # Send trace data to collector
        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
            json.dump(otlp_payload, f)
            payload_file = f.name

        try:
            result = subprocess.run(
                [
                    "curl",
                    "-X",
                    "POST",
                    "-H",
                    "Content-Type: application/json",
                    "-d",
                    f"@{payload_file}",
                    "-s",
                    "-w",
                    "\n%{http_code}",
                    "http://localhost:4318/v1/traces",
                ],
                capture_output=True,
                text=True,
                timeout=10,
            )

            assert result.returncode == 0, f"Failed to send OTLP data: {result.stderr}"

            # Parse response (last line is HTTP code)
            lines = result.stdout.strip().split("\n")
            http_code = lines[-1]

            # 200/202 indicate successful processing
            assert http_code in ["200", "202"], (
                f"Collector failed to process OTLP data. HTTP {http_code}\n"
                f"Response: {result.stdout}"
            )

            # Verify data was actually processed by checking debug exporter logs
            logs_result = subprocess.run(
                ["docker", "logs", "mca-otel-collector", "--tail", "50"],
                capture_output=True,
                text=True,
                timeout=10,
            )
            assert logs_result.returncode == 0, "Failed to retrieve collector logs"

            # Debug exporter should show trace data (look for trace ID or "test-span")
            assert (
                "5b8efff798038103d269b633813fc60c" in logs_result.stdout
                or "test-span" in logs_result.stdout
            ), (
                "Collector accepted OTLP data but did not process it. "
                "Debug exporter logs do not contain expected trace data."
            )
        finally:
            if os.path.exists(payload_file):
                os.unlink(payload_file)

    def test_container_security_hardening(self):
        """Test that container has security hardening enabled."""
        result = subprocess.run(
            [
                "docker",
                "inspect",
                "--format",
                "{{json .HostConfig}}",
                "mca-otel-collector",
            ],
            capture_output=True,
            text=True,
        )
        assert result.returncode == 0, "Failed to inspect container"

        host_config = json.loads(result.stdout)

        # Verify read-only root filesystem
        assert (
            host_config.get("ReadonlyRootfs") is True
        ), "Container root filesystem is not read-only"

        # Verify capabilities dropped
        assert "ALL" in host_config.get("CapDrop", []), "Container has not dropped all capabilities"

        # Verify no-new-privileges
        security_opt = host_config.get("SecurityOpt", [])
        assert any(
            "no-new-privileges:true" in opt for opt in security_opt
        ), "Container does not have no-new-privileges enabled"

    def test_tmpfs_mounts_configured(self):
        """Test that tmpfs mounts are configured for writable directories."""
        result = subprocess.run(
            [
                "docker",
                "inspect",
                "--format",
                "{{json .HostConfig.Tmpfs}}",
                "mca-otel-collector",
            ],
            capture_output=True,
            text=True,
        )
        assert result.returncode == 0, "Failed to inspect tmpfs mounts"

        tmpfs = json.loads(result.stdout) or {}

        # Verify /tmp and /var/lib/otelcol are tmpfs
        assert "/tmp" in tmpfs, "/tmp is not mounted as tmpfs"
        assert "/var/lib/otelcol" in tmpfs, "/var/lib/otelcol is not mounted as tmpfs"

    def test_container_restart_policy(self):
        """
        Test that container has correct restart policy.

        Validates:
        - Restart policy is 'unless-stopped'
        """
        result = subprocess.run(
            [
                "docker",
                "inspect",
                "--format",
                "{{.HostConfig.RestartPolicy.Name}}",
                "mca-otel-collector",
            ],
            capture_output=True,
            text=True,
        )

        assert result.returncode == 0, "Failed to inspect restart policy"

        restart_policy = result.stdout.strip()
        assert restart_policy == "unless-stopped", f"Unexpected restart policy: {restart_policy}"

    def test_config_changes_visible_after_restart(self, project_root):
        """
        Test that config file changes on host are visible after container restart.

        Validates:
        - Host config changes are reflected in container
        - Restart is required for changes to take effect (no hot reload)
        """
        config_path = project_root / "config" / "otel-collector-config.yaml"

        # Backup original config
        with tempfile.NamedTemporaryFile(mode="w", delete=False) as backup:
            with open(config_path) as original:
                shutil.copyfile(config_path, backup.name)
            backup_path = backup.name

        try:
            # Read current config from container
            before_result = subprocess.run(
                [
                    "docker",
                    "exec",
                    "mca-otel-collector",
                    "cat",
                    "/etc/otelcol-contrib/config.yaml",
                ],
                capture_output=True,
                text=True,
            )
            assert before_result.returncode == 0, "Failed to read config before modification"

            # Modify host config (add comment marker)
            test_marker = "# TEST_CONFIG_RELOAD_MARKER"
            with open(config_path, "a") as f:
                f.write(f"\n{test_marker}\n")

            # Restart container to pick up changes
            restart_result = subprocess.run(
                ["docker", "restart", "mca-otel-collector"],
                capture_output=True,
                timeout=30,
            )
            assert restart_result.returncode == 0, "Failed to restart container"

            # Wait for container to be ready (poll health status)
            max_wait = 60
            start_time = time.time()
            while time.time() - start_time < max_wait:
                health_result = subprocess.run(
                    [
                        "docker",
                        "inspect",
                        "--format={{.State.Health.Status}}",
                        "mca-otel-collector",
                    ],
                    capture_output=True,
                    text=True,
                )
                if health_result.returncode == 0 and "healthy" in health_result.stdout:
                    break
                time.sleep(2)
            else:
                raise TimeoutError("Container did not become healthy within 60s after restart")

            # Read config after restart
            after_result = subprocess.run(
                [
                    "docker",
                    "exec",
                    "mca-otel-collector",
                    "cat",
                    "/etc/otelcol-contrib/config.yaml",
                ],
                capture_output=True,
                text=True,
            )
            assert after_result.returncode == 0, "Failed to read config after modification"

            # Verify test marker is present
            assert test_marker in after_result.stdout, (
                "Config changes not visible after restart. "
                "Volume mount may not be working correctly."
            )

        finally:
            # Restore original config
            shutil.copyfile(backup_path, config_path)
            os.unlink(backup_path)

            # Restart to restore original config
            subprocess.run(["docker", "restart", "mca-otel-collector"], capture_output=True)
            # Wait for container to stabilize after config restore
            time.sleep(5)
