"""
tests.test_sdk -- Unit tests for the neuroflow Python SDK.

Tests cover:
- Package imports and public API surface
- Pydantic model creation and validation
- Features parsing from raw dictionaries
- LocalClient with synthetic (simulated) device
- Device lifecycle (connect, stream, get data, disconnect)
- Feature extraction from live synthetic data

All tests run without a server by using LocalClient with
BrainFlow's synthetic board (board_id=-1).
"""

from __future__ import annotations

import sys
import time

import pytest


# ──────────────────────────────────────────────────────────────
# Import Tests
# ──────────────────────────────────────────────────────────────

class TestImports:
    """Verify that the SDK package and all public symbols are importable."""

    def test_import_neuroflow(self) -> None:
        """The top-level neuroflow package should import without errors."""
        import neuroflow
        assert hasattr(neuroflow, "__version__")
        assert neuroflow.__version__ == "0.1.0"

    def test_import_client(self) -> None:
        """Client and AsyncClient classes should be importable."""
        from neuroflow import Client, AsyncClient
        assert Client is not None
        assert AsyncClient is not None

    def test_import_local_client(self) -> None:
        """LocalClient class should be importable."""
        from neuroflow import LocalClient
        assert LocalClient is not None

    def test_import_models(self) -> None:
        """All model classes should be importable from the top level."""
        from neuroflow import (
            Device,
            DeviceStatus,
            Session,
            SessionStatus,
            SessionData,
            StreamData,
            StreamMessageType,
            Features,
            PipelineInfo,
            SystemStatus,
        )
        assert Device is not None
        assert DeviceStatus.CONNECTED == "connected"
        assert SessionStatus.RECORDING == "recording"
        assert StreamMessageType.EEG_DATA == "eeg_data"

    def test_import_device_classes(self) -> None:
        """Device abstraction classes should be importable."""
        from neuroflow import RemoteDevice, LocalDevice
        assert RemoteDevice is not None
        assert LocalDevice is not None

    def test_import_stream_classes(self) -> None:
        """Streaming classes should be importable."""
        from neuroflow import DataStream, SyncDataStream
        assert DataStream is not None
        assert SyncDataStream is not None

    def test_import_session_handle(self) -> None:
        """SessionHandle should be importable."""
        from neuroflow import SessionHandle
        assert SessionHandle is not None

    def test_import_exception(self) -> None:
        """NeuroFlowAPIError should be importable."""
        from neuroflow import NeuroFlowAPIError
        assert issubclass(NeuroFlowAPIError, Exception)

    def test_all_exports(self) -> None:
        """__all__ should list every public symbol."""
        import neuroflow
        assert len(neuroflow.__all__) >= 15
        for name in neuroflow.__all__:
            assert hasattr(neuroflow, name), f"neuroflow.__all__ lists '{name}' but it is not an attribute"


# ──────────────────────────────────────────────────────────────
# Model Tests
# ──────────────────────────────────────────────────────────────

class TestModels:
    """Test Pydantic model creation, validation, and serialization."""

    def test_device_creation(self) -> None:
        """Device model should accept all fields and set defaults."""
        from neuroflow import Device, DeviceStatus

        device = Device(device_id="dev_test123")
        assert device.device_id == "dev_test123"
        assert device.status == DeviceStatus.DISCONNECTED
        assert device.sampling_rate == 0
        assert device.board_id == -1

    def test_device_with_all_fields(self) -> None:
        """Device model should accept all explicit fields."""
        from neuroflow import Device, DeviceStatus

        device = Device(
            device_id="dev_abc",
            name="Muse S",
            manufacturer="Interaxon",
            board_id=22,
            status=DeviceStatus.STREAMING,
            sampling_rate=256,
            num_channels=4,
            connection_type="ble",
            is_streaming=True,
            total_samples=10000,
            signal_quality=0.92,
            uptime_seconds=120.5,
        )
        assert device.name == "Muse S"
        assert device.is_streaming is True
        assert device.num_channels == 4
        assert device.signal_quality == 0.92

    def test_device_repr(self) -> None:
        """Device repr should be human-readable."""
        from neuroflow import Device, DeviceStatus

        device = Device(device_id="dev_x", name="Test", status=DeviceStatus.CONNECTED)
        r = repr(device)
        assert "dev_x" in r
        assert "Test" in r

    def test_session_creation(self) -> None:
        """Session model should accept all fields."""
        from neuroflow import Session, SessionStatus

        session = Session(
            session_id="ses_abc123",
            device_id="dev_xyz",
            status=SessionStatus.RECORDING,
            pipeline_name="meditation",
        )
        assert session.session_id == "ses_abc123"
        assert session.is_active is True
        assert session.pipeline_name == "meditation"

    def test_session_is_active(self) -> None:
        """Session.is_active should reflect RECORDING status."""
        from neuroflow import Session, SessionStatus

        active = Session(session_id="s1", status=SessionStatus.RECORDING)
        assert active.is_active is True

        stopped = Session(session_id="s2", status=SessionStatus.COMPLETED)
        assert stopped.is_active is False

    def test_stream_data_creation(self) -> None:
        """StreamData model should accept EEG data and metadata."""
        from neuroflow import StreamData, StreamMessageType

        data = StreamData(
            timestamp=time.time(),
            message_type=StreamMessageType.EEG_DATA,
            eeg=[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]],
            channel_names=["Fp1", "Fp2"],
            num_samples=3,
            sampling_rate=250,
        )
        assert len(data.eeg) == 2
        assert data.num_samples == 3
        assert data.channel_names == ["Fp1", "Fp2"]

    def test_features_creation(self) -> None:
        """Features model should accept categorized feature dicts."""
        from neuroflow import Features

        features = Features(
            band_powers={"avg_alpha_power": 12.5, "avg_beta_power": 8.3},
            ratios={"avg_theta_beta_ratio": 1.5},
        )
        assert features.band_powers["avg_alpha_power"] == 12.5
        assert features.ratios["avg_theta_beta_ratio"] == 1.5

    def test_features_from_raw(self) -> None:
        """Features.from_raw_features should categorize features correctly."""
        from neuroflow import Features

        raw = {
            "avg_alpha_power": 15.0,
            "avg_beta_power": 10.0,
            "avg_theta_beta_ratio": 2.1,
            "ch0_dominant_freq": 10.5,
            "ch0_spectral_entropy": 3.2,
            "ch0_mean": 0.1,
            "ch0_std": 25.3,
            "ch0_hjorth_activity": 640.0,
            "avg_delta_power": 20.0,
            "some_string_feature": "not_a_number",
        }
        features = Features.from_raw_features(raw, session_id="test_session")

        # Band powers should include alpha, beta, delta
        assert "avg_alpha_power" in features.band_powers
        assert "avg_beta_power" in features.band_powers
        assert "avg_delta_power" in features.band_powers

        # Ratios
        assert "avg_theta_beta_ratio" in features.ratios

        # Spectral
        assert "ch0_dominant_freq" in features.spectral
        assert "ch0_spectral_entropy" in features.spectral

        # Temporal
        assert "ch0_mean" in features.temporal
        assert "ch0_std" in features.temporal
        assert "ch0_hjorth_activity" in features.temporal

        # String features should be excluded (not numeric)
        assert "some_string_feature" not in features.raw or not isinstance(
            features.raw.get("some_string_feature"), (int, float)
        )

        # Session ID should be set
        assert features.session_id == "test_session"

    def test_session_data_creation(self) -> None:
        """SessionData model should hold complete session data."""
        from neuroflow import SessionData

        sd = SessionData(
            session_id="ses_test",
            sampling_rate=250,
            num_channels=4,
            num_samples=1000,
            duration_seconds=4.0,
            eeg_data=[[0.0] * 1000] * 4,
            timestamps=list(range(1000)),
        )
        assert sd.num_channels == 4
        assert sd.duration_seconds == 4.0
        assert len(sd.eeg_data) == 4

    def test_pipeline_info(self) -> None:
        """PipelineInfo model should hold pipeline metadata."""
        from neuroflow import PipelineInfo

        pi = PipelineInfo(
            name="meditation",
            steps=["bandpass", "notch"],
            feature_extractors=["band_powers", "spectral"],
            is_preset=True,
        )
        assert pi.name == "meditation"
        assert pi.is_preset is True

    def test_system_status(self) -> None:
        """SystemStatus model should hold server status info."""
        from neuroflow import SystemStatus

        status = SystemStatus(
            version="0.1.0",
            uptime_seconds=100.0,
            active_devices=2,
            active_sessions=1,
        )
        assert status.version == "0.1.0"
        assert status.active_devices == 2

    def test_model_serialization(self) -> None:
        """Models should serialize to dict and JSON."""
        from neuroflow import Device, DeviceStatus

        device = Device(
            device_id="dev_ser",
            name="Test",
            status=DeviceStatus.CONNECTED,
        )
        d = device.model_dump()
        assert d["device_id"] == "dev_ser"
        assert d["status"] == "connected"

        j = device.model_dump_json()
        assert "dev_ser" in j
        assert "connected" in j


# ──────────────────────────────────────────────────────────────
# Enum Tests
# ──────────────────────────────────────────────────────────────

class TestEnums:
    """Test that enums have the expected values."""

    def test_device_status_values(self) -> None:
        from neuroflow import DeviceStatus
        assert DeviceStatus.DISCONNECTED == "disconnected"
        assert DeviceStatus.CONNECTING == "connecting"
        assert DeviceStatus.CONNECTED == "connected"
        assert DeviceStatus.STREAMING == "streaming"
        assert DeviceStatus.ERROR == "error"

    def test_session_status_values(self) -> None:
        from neuroflow import SessionStatus
        assert SessionStatus.CREATED == "created"
        assert SessionStatus.RECORDING == "recording"
        assert SessionStatus.STOPPED == "stopped"
        assert SessionStatus.COMPLETED == "completed"

    def test_stream_message_types(self) -> None:
        from neuroflow import StreamMessageType
        assert StreamMessageType.EEG_DATA == "eeg_data"
        assert StreamMessageType.FEATURES == "features"
        assert StreamMessageType.QUALITY == "quality"


# ──────────────────────────────────────────────────────────────
# Client Construction Tests
# ──────────────────────────────────────────────────────────────

class TestClientConstruction:
    """Test client object construction (without network calls)."""

    def test_client_repr(self) -> None:
        """Client repr should include the URL."""
        from neuroflow import Client
        client = Client(api_url="http://example.com:8000")
        r = repr(client)
        assert "example.com:8000" in r
        client.close()

    def test_client_with_api_key_repr(self) -> None:
        """Client repr should indicate authentication status."""
        from neuroflow import Client
        client = Client(api_url="http://localhost:8000", api_key="nf_test123")
        r = repr(client)
        assert "authenticated" in r
        client.close()

    def test_client_context_manager(self) -> None:
        """Client should work as a context manager."""
        from neuroflow import Client
        with Client(api_url="http://localhost:8000") as client:
            assert client is not None

    def test_api_error_construction(self) -> None:
        """NeuroFlowAPIError should store status code and detail."""
        from neuroflow import NeuroFlowAPIError
        err = NeuroFlowAPIError(404, "Device not found")
        assert err.status_code == 404
        assert err.detail == "Device not found"
        assert "404" in str(err)
        assert "Device not found" in str(err)


# ──────────────────────────────────────────────────────────────
# LocalClient Tests (with synthetic device)
# ──────────────────────────────────────────────────────────────

class TestLocalClient:
    """
    Integration tests using LocalClient with a synthetic BrainFlow board.

    These tests require brainflow and neuroflow to be installed, but do
    NOT require any physical hardware -- they use board_id=-1 which
    generates simulated EEG data.
    """

    @pytest.fixture
    def client(self):
        """Create a LocalClient and clean up after test."""
        from neuroflow import LocalClient
        c = LocalClient()
        yield c
        c.close()

    def test_local_client_creation(self, client) -> None:
        """LocalClient should create without errors."""
        assert repr(client) is not None
        assert "0 device" in repr(client)

    def test_list_supported_boards(self) -> None:
        """list_supported_boards should return common board IDs."""
        from neuroflow import LocalClient
        boards = LocalClient.list_supported_boards()
        assert boards["synthetic"] == -1
        assert boards["cyton"] == 0
        assert "muse_s_bled" in boards

    def test_connect_synthetic(self, client) -> None:
        """Connecting a synthetic board should succeed."""
        device = client.connect(board_id=-1)
        assert device.is_connected
        assert device.board_id == -1
        assert len(device.id) > 0
        device.disconnect()

    def test_list_devices(self, client) -> None:
        """list_devices should return connected devices."""
        assert len(client.list_devices()) == 0
        device = client.connect(board_id=-1)
        assert len(client.list_devices()) == 1
        device.disconnect()

    def test_get_device(self, client) -> None:
        """get_device should retrieve a connected device by ID."""
        device = client.connect(board_id=-1)
        retrieved = client.get_device(device.id)
        assert retrieved.id == device.id
        device.disconnect()

    def test_get_device_not_found(self, client) -> None:
        """get_device should raise KeyError for unknown device IDs."""
        with pytest.raises(KeyError):
            client.get_device("nonexistent_device")

    def test_device_start_stop_stream(self, client) -> None:
        """Device should start and stop streaming."""
        device = client.connect(board_id=-1)
        assert not device.is_streaming

        device.start_stream()
        assert device.is_streaming

        device.stop_stream()
        assert not device.is_streaming

        device.disconnect()

    def test_device_get_data(self, client) -> None:
        """Device should return data after streaming."""
        device = client.connect(board_id=-1)
        device.start_stream()

        # Let some data accumulate
        time.sleep(0.5)

        data = device.get_current_data(seconds=0.3)
        assert data.num_samples > 0
        assert len(data.eeg) > 0
        assert len(data.channel_names) > 0
        assert data.sampling_rate > 0

        device.stop_stream()
        device.disconnect()

    def test_device_get_features(self, client) -> None:
        """Device should extract features from streaming data."""
        device = client.connect(board_id=-1)
        device.start_stream()

        # Accumulate enough data for feature extraction
        time.sleep(1.0)

        features = device.get_features(seconds=0.8)
        assert isinstance(features.raw, dict)

        # With enough data, we should get band power features
        if features.band_powers:
            # Check that standard bands are present
            has_alpha = any("alpha" in k for k in features.band_powers)
            has_beta = any("beta" in k for k in features.band_powers)
            assert has_alpha or has_beta, (
                f"Expected alpha or beta band powers, got: {list(features.band_powers.keys())[:5]}"
            )

        device.stop_stream()
        device.disconnect()

    def test_device_quality_scores(self, client) -> None:
        """Device data should include signal quality information."""
        device = client.connect(board_id=-1)
        device.start_stream()
        time.sleep(0.5)

        data = device.get_current_data(seconds=0.3)
        # Quality should be available for synthetic data
        assert isinstance(data.quality, dict)
        assert isinstance(data.overall_quality, float)

        device.stop_stream()
        device.disconnect()

    def test_device_recording(self, client) -> None:
        """Device should support start/stop recording."""
        device = client.connect(board_id=-1)
        device.start_stream()
        time.sleep(0.3)

        device.start_recording(session_id="test_recording")
        time.sleep(0.5)
        # Must call get_data to trigger recording buffer append
        device.get_data()
        recording = device.stop_recording()

        assert recording.num_samples > 0
        assert len(recording.eeg) > 0

        device.stop_stream()
        device.disconnect()

    def test_device_context_manager(self, client) -> None:
        """LocalDevice should work as a context manager."""
        device = client.connect(board_id=-1)
        with device:
            device.start_stream()
            time.sleep(0.2)
        # After exiting, device should be disconnected
        assert not device.is_streaming

    def test_client_disconnect_all(self) -> None:
        """disconnect_all should cleanly shut down all devices."""
        from neuroflow import LocalClient
        client = LocalClient()
        d1 = client.connect(board_id=-1)
        d1.start_stream()
        time.sleep(0.2)

        client.disconnect_all()
        assert len(client.list_devices()) == 0

    def test_client_context_manager(self) -> None:
        """LocalClient should work as a context manager."""
        from neuroflow import LocalClient
        with LocalClient() as client:
            device = client.connect(board_id=-1)
            device.start_stream()
            time.sleep(0.2)
        # After exiting context, devices should be cleaned up

    def test_device_repr(self, client) -> None:
        """Device repr should be informative."""
        device = client.connect(board_id=-1)
        r = repr(device)
        assert "LocalDevice" in r
        assert device.id in r
        device.disconnect()

    def test_device_properties(self, client) -> None:
        """Device properties should return sensible values."""
        device = client.connect(board_id=-1)
        assert device.sampling_rate > 0
        assert device.name != ""
        assert device.status in ("connected", "streaming", "disconnected")
        assert isinstance(device.total_samples, int)
        device.disconnect()


# ──────────────────────────────────────────────────────────────
# Stream Module Tests
# ──────────────────────────────────────────────────────────────

class TestStreamModule:
    """Test stream module construction (without actual WebSocket)."""

    def test_data_stream_construction(self) -> None:
        """DataStream should construct without connecting."""
        from neuroflow.stream import DataStream
        stream = DataStream(
            ws_url="ws://localhost:8000/ws/stream/dev_test",
            device_id="dev_test",
            channels=["eeg_data", "features"],
            pipeline_name="meditation",
        )
        assert not stream.is_running

    def test_data_stream_stop(self) -> None:
        """DataStream.stop() should set running to False."""
        from neuroflow.stream import DataStream
        stream = DataStream(
            ws_url="ws://localhost:8000/ws/stream/dev_test",
            device_id="dev_test",
        )
        stream.stop()
        assert not stream.is_running

    def test_data_stream_parse_eeg_message(self) -> None:
        """DataStream should parse EEG WebSocket messages."""
        import json
        from neuroflow.stream import DataStream

        stream = DataStream(
            ws_url="ws://localhost:8000/ws/stream/dev_test",
            device_id="dev_test",
        )
        msg = json.dumps({
            "type": "eeg_data",
            "timestamp": 1234567890.0,
            "data": {
                "eeg": [[1.0, 2.0], [3.0, 4.0]],
                "channels": ["Fp1", "Fp2"],
                "samples": 2,
                "sampling_rate": 250,
            },
        })
        parsed = stream._parse_message(msg)
        assert parsed is not None
        assert parsed.num_samples == 2
        assert parsed.eeg == [[1.0, 2.0], [3.0, 4.0]]
        assert parsed.channel_names == ["Fp1", "Fp2"]

    def test_data_stream_parse_features_message(self) -> None:
        """DataStream should parse features WebSocket messages."""
        import json
        from neuroflow.stream import DataStream

        stream = DataStream(
            ws_url="ws://localhost:8000/ws/stream/dev_test",
            device_id="dev_test",
        )
        msg = json.dumps({
            "type": "features",
            "timestamp": 1234567890.0,
            "data": {
                "avg_alpha_power": 15.0,
                "avg_theta_beta_ratio": 2.1,
            },
        })
        parsed = stream._parse_message(msg)
        assert parsed is not None
        assert parsed.features["avg_alpha_power"] == 15.0

    def test_data_stream_parse_quality_message(self) -> None:
        """DataStream should parse quality WebSocket messages."""
        import json
        from neuroflow.stream import DataStream

        stream = DataStream(
            ws_url="ws://localhost:8000/ws/stream/dev_test",
            device_id="dev_test",
        )
        msg = json.dumps({
            "type": "quality",
            "timestamp": 1234567890.0,
            "data": {
                "channel_scores": [0.9, 0.85, 0.92, 0.88],
                "overall": 0.89,
            },
        })
        parsed = stream._parse_message(msg)
        assert parsed is not None
        assert parsed.overall_quality == 0.89
        assert len(parsed.quality) == 4

    def test_data_stream_parse_invalid_json(self) -> None:
        """DataStream should return None for invalid JSON."""
        from neuroflow.stream import DataStream
        stream = DataStream(
            ws_url="ws://localhost:8000/ws/stream/dev_test",
            device_id="dev_test",
        )
        assert stream._parse_message("not json") is None

    def test_data_stream_parse_unknown_type(self) -> None:
        """DataStream should return None for unknown message types."""
        import json
        from neuroflow.stream import DataStream
        stream = DataStream(
            ws_url="ws://localhost:8000/ws/stream/dev_test",
            device_id="dev_test",
        )
        msg = json.dumps({"type": "pong", "timestamp": 123.0})
        assert stream._parse_message(msg) is None
