"""Integration tests for Conbus reverse proxy functionality."""

import socket
import tempfile
import threading
import time
from pathlib import Path
from typing import Optional

import pytest

from xp.services.reverse_proxy_service import ReverseProxyService


class MockServer:
    """Mock Conbus server for testing reverse proxy integration"""

    def __init__(self, port: int):
        self.port = port
        self.socket: Optional[socket.socket] = None
        self.is_running = False
        self.received_messages: list[str] = []
        self.responses: list[str] = []

    def start(self):
        """Start the mock server"""
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.bind(("127.0.0.1", self.port))
        self.socket.listen(1)
        self.is_running = True

        # Handle connections in background thread
        thread = threading.Thread(target=self._accept_connections, daemon=True)
        thread.start()

    def stop(self):
        """Stop the mock server"""
        self.is_running = False
        if self.socket:
            self.socket.close()

    def add_response(self, response: str) -> None:
        """Add a response to send when receiving messages"""
        self.responses.append(response)

    def _accept_connections(self):
        """Accept and handle client connections"""
        while self.is_running:
            try:
                if not self.socket:
                    raise socket.error
                client_socket, address = self.socket.accept()
                self._handle_client(client_socket)
            except (OSError, socket.error):
                break

    def _handle_client(self, client_socket):
        """Handle individual client connection"""
        try:
            while self.is_running:
                data = client_socket.recv(1024)
                if not data:
                    break

                message = data.decode("latin-1").strip()
                self.received_messages.append(message)

                # Send response if available
                if self.responses:
                    response = self.responses.pop(0)
                    client_socket.send(response.encode("latin-1"))

        except (ValueError, KeyError, ConnectionError):
            pass
        finally:
            client_socket.close()


class TestReverseProxyIntegration:
    """Integration tests for reverse proxy with mock server and client"""

    def setup_method(self):
        """Set up test environment"""
        # Use high port numbers to avoid conflicts
        self.proxy_port = 19001
        self.server_port = 19002

        # Create mock server
        self.mock_server = MockServer(self.server_port)

        # Create temporary config for proxy
        self.temp_config = tempfile.NamedTemporaryFile(
            mode="w", suffix=".yml", delete=False
        )
        self.temp_config.write(
            f"""
conbus:
  ip: 127.0.0.1
  port: {self.server_port}
  timeout: 2
"""
        )
        self.temp_config.close()

        # Create reverse proxy
        self.proxy = ReverseProxyService(
            config_path=self.temp_config.name, listen_port=self.proxy_port
        )

    def teardown_method(self):
        """Clean up test environment"""
        if self.proxy.is_running:
            self.proxy.stop_proxy()

        if self.mock_server.is_running:
            self.mock_server.stop()

        if Path(self.temp_config.name).exists():
            Path(self.temp_config.name).unlink()

    @pytest.mark.reverseproxy
    def test_end_to_end_telegram_relay(self):
        """Test complete telegram relay from client through proxy to server"""
        # Start mock server
        self.mock_server.start()
        time.sleep(0.1)  # Give server time to start

        # Add expected response
        self.mock_server.add_response("<R0012345011F01DFM>")

        # Start proxy
        result = self.proxy.start_proxy()
        assert result.success
        time.sleep(0.1)  # Give proxy time to start

        try:
            # Create client and send telegram
            client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            client_socket.connect(("127.0.0.1", self.proxy_port))

            # Send discover telegram
            discover_telegram = "<S0000000000F01D00FA>"
            client_socket.send(discover_telegram.encode("latin-1"))

            # Receive response
            response = client_socket.recv(1024).decode("latin-1").strip()

            # Verify end-to-end communication
            assert response == "<R0012345011F01DFM>"
            assert discover_telegram in self.mock_server.received_messages

            client_socket.close()

        finally:
            self.proxy.stop_proxy()
            self.mock_server.stop()

    @pytest.mark.reverseproxy
    def test_proxy_connection_failure_handling(self):
        """Test proxy behavior when target server is unavailable"""
        # Don't start mock server - simulate server unavailable

        # Start proxy
        result = self.proxy.start_proxy()
        assert result.success
        time.sleep(0.1)

        try:
            # Try to connect client - this should fail gracefully
            client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            client_socket.settimeout(1.0)  # Short timeout for test

            # Connect to proxy
            client_socket.connect(("127.0.0.1", self.proxy_port))

            # Send telegram - connection to server should fail
            client_socket.send("<S0000000000F01D00FA>".encode("latin-1"))

            # Connection should be closed due to server unavailability
            time.sleep(0.5)  # Give time for connection handling

            # Verify proxy is still running despite connection failure
            status = self.proxy.get_status()
            assert status.success
            assert status.data["running"]

            client_socket.close()

        finally:
            self.proxy.stop_proxy()

    @pytest.mark.reverseproxy
    def test_bidirectional_data_relay(self):
        """Test bidirectional data relay between client and server"""
        # Start mock server with multiple responses
        self.mock_server.start()
        time.sleep(0.1)

        # Add multiple responses to simulate conversation
        responses = [
            "<R0012345011F01DFM>",
            "<R0012345011F02D02XP230_V1.00.04FI>",
            "<R0012345011F02D20+12.5V§OK>",
        ]
        for response in responses:
            self.mock_server.add_response(response)

        # Start proxy
        result = self.proxy.start_proxy()
        assert result.success
        time.sleep(0.1)

        try:
            # Create client connection
            client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            client_socket.connect(("127.0.0.1", self.proxy_port))

            # Send multiple telegrams
            telegrams = [
                "<S0000000000F01D00FA>",
                "<S0012345011F02D02FM>",
                "<S0012345011F02D20FM>",
            ]

            received_responses = []
            for telegram in telegrams:
                client_socket.send(telegram.encode("latin-1"))
                time.sleep(0.1)  # Small delay between sends

                # Receive response
                response = client_socket.recv(1024).decode("latin-1").strip()
                received_responses.append(response)

            client_socket.close()

            # Verify all telegrams were relayed to server
            for telegram in telegrams:
                assert telegram in self.mock_server.received_messages

            # Verify all responses were relayed back to client
            assert received_responses == responses[: len(received_responses)]

        finally:
            self.proxy.stop_proxy()
            self.mock_server.stop()

    @pytest.mark.reverseproxy
    def test_proxy_status_tracking(self):
        """Test proxy status tracking during connections"""
        # Start mock server
        self.mock_server.start()
        time.sleep(0.1)

        # Start proxy
        result = self.proxy.start_proxy()
        assert result.success
        time.sleep(0.1)

        try:
            # Check initial status
            status = self.proxy.get_status()
            assert status.success
            assert status.data["running"]
            assert status.data["active_connections"] == 0

            # Create client connection
            client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            client_socket.connect(("127.0.0.1", self.proxy_port))
            time.sleep(0.1)  # Give time for connection to be established

            # Check status with active connection
            # Note: Connection tracking happens in background threads
            # so we might not see it immediately in a unit test

            client_socket.close()
            time.sleep(0.1)  # Give time for cleanup

        finally:
            self.proxy.stop_proxy()
            self.mock_server.stop()


class TestReverseProxyErrorHandling:
    """Test error handling scenarios for reverse proxy"""

    @pytest.mark.reverseproxy
    def test_proxy_start_port_already_in_use(self):
        """Test proxy startup when port is already in use"""
        # Create a socket to occupy the port
        blocking_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        blocking_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        blocking_socket.bind(("0.0.0.0", 19003))
        blocking_socket.listen(1)

        try:
            # Try to start proxy on the same port
            proxy = ReverseProxyService(listen_port=19003)
            result = proxy.start_proxy()

            # Should fail due to port conflict
            assert not result.success
            assert result.error is not None and (
                "Address already in use" in result.error
                or "permission" in result.error.lower()
            )

        finally:
            blocking_socket.close()

    @pytest.mark.reverseproxy
    def test_invalid_configuration_handling(self):
        """Test proxy behavior with invalid configuration"""
        # Create config with invalid target
        temp_config = tempfile.NamedTemporaryFile(mode="w", suffix=".yml", delete=False)
        temp_config.write(
            """
conbus:
  ip: "999.999.999.999"  # Invalid IP
  port: 99999            # Invalid port
  timeout: -1            # Invalid timeout
"""
        )
        temp_config.close()

        try:
            proxy = ReverseProxyService(config_path=temp_config.name, listen_port=19004)

            # Should load config but connections will fail
            assert proxy.target_ip == "999.999.999.999"
            assert proxy.target_port == 99999

        finally:
            Path(temp_config.name).unlink()
