from __future__ import annotations

import socket
import time
import os

import pytest

from pulselog.config import Config
from pulselog.levels import Level
from pulselog.record import LogRecord
from pulselog.server.websocket import DashboardServer, _find_free_port


def make_record(msg: str = "test") -> LogRecord:
    return LogRecord(level=Level.INFO, msg=msg, timestamp=time.time(), extra={})


def _free_port() -> int:
    """Pick a truly free port for testing."""
    with socket.socket() as s:
        s.bind(("localhost", 0))
        return s.getsockname()[1]


class TestFindFreePort:
    def test_returns_free_port(self):
        port = _find_free_port("localhost", 15000)
        assert port is not None
        assert 15000 <= port < 15100

    def test_skips_occupied_port(self):
        # Occupy a port then ask starting from it
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            s.bind(("localhost", 0))
            s.listen(1)
            occupied = s.getsockname()[1]
            result = _find_free_port("localhost", occupied)
            assert result != occupied


class TestDashboardServer:
    def setup_method(self):
        os.environ["CI"] = "true"

    def teardown_method(self):
        os.environ.pop("CI", None)

    def _make_config(self, port: int) -> Config:
        return Config.load(
            name="test",
            host="localhost",
            port=port,
            auto_open=False,
            dashboard=True,
        )

    def test_server_starts_and_port_bound(self):
        port = _free_port()
        cfg = self._make_config(port)
        srv = DashboardServer(cfg)
        srv.start()
        time.sleep(0.4)

        with socket.socket() as s:
            result = s.connect_ex(("localhost", srv.port))
        srv.shutdown()
        assert result == 0, "Server port not bound after start()"

    def test_broadcast_no_crash_without_clients(self):
        port = _free_port()
        cfg = self._make_config(port)
        srv = DashboardServer(cfg)
        srv.start()
        time.sleep(0.3)

        # No clients — broadcast must not raise
        batch = [make_record("test-msg")]
        srv.broadcast(batch)  # must not raise
        srv.shutdown()

    def test_history_populated_via_broadcast(self):
        port = _free_port()
        cfg = self._make_config(port)
        srv = DashboardServer(cfg)
        srv.start()
        time.sleep(0.3)

        batch = [make_record(f"hist {i}") for i in range(5)]
        srv.broadcast(batch)

        # History should be populated
        import json
        with srv._history_lock:
            history = list(srv._history)
        msgs = [r["msg"] for r in history]
        for i in range(5):
            assert f"hist {i}" in msgs
        srv.shutdown()

    def test_shutdown_is_idempotent(self):
        port = _free_port()
        cfg = self._make_config(port)
        srv = DashboardServer(cfg)
        srv.start()
        time.sleep(0.2)
        srv.shutdown()
        srv.shutdown()  # second shutdown must not raise

    def test_client_count_initially_zero(self):
        port = _free_port()
        cfg = self._make_config(port)
        srv = DashboardServer(cfg)
        assert srv.client_count == 0
        srv.start()
        time.sleep(0.2)
        assert srv.client_count == 0
        srv.shutdown()

    def test_port_auto_increment(self):
        """If preferred port is occupied, server must pick the next free one."""
        # Occupy a port
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as blocker:
            blocker.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            blocker.bind(("localhost", 0))
            blocker.listen(1)
            occupied = blocker.getsockname()[1]

            cfg = self._make_config(occupied)
            srv = DashboardServer(cfg)
            srv.start()
            time.sleep(0.3)

            assert srv.port != occupied
            srv.shutdown()
