"""Tests for dataset converters."""

from __future__ import annotations

import pytest

from radiens_core._converters._dataset import (
    file_type_from_proto,
    file_type_to_proto,
    protocol_to_proto,
    transform_edge_to_proto,
    transform_node_to_proto,
)
from radiens_core._generated import common_pb2
from radiens_core.models.dataset import (
    BulkSinkParams,
    BulkSourceParams,
    CARParams,
    HighLowPassParams,
    RadiensFileType,
    SinkParams,
    SourceParams,
    TransformEdge,
    TransformNode,
    TransformNodeType,
)


class TestFileTypeRoundTrip:
    @pytest.mark.parametrize("ft", list(RadiensFileType))
    def test_round_trip(self, ft: RadiensFileType) -> None:
        """Every RadiensFileType must survive a to_proto → from_proto round-trip."""
        proto_val = file_type_to_proto(ft)
        result = file_type_from_proto(int(proto_val))
        assert result == ft


class TestTransformEdgeToProto:
    def test_fields_preserved(self) -> None:
        edge = TransformEdge(id="e1", source="n1", target="n2")
        pb = transform_edge_to_proto(edge)
        assert pb.id == "e1"
        assert pb.source == "n1"
        assert pb.target == "n2"


class TestTransformNodeToProto:
    def test_highpass(self) -> None:
        node = TransformNode.highpass(frequency=300.0)
        pb = transform_node_to_proto(node)
        assert pb.type == common_pb2.HIGHPASS_FILTER
        assert pb.highLowPassParams.frequency == 300.0
        assert not pb.highLowPassParams.HasField("order")

    def test_highpass_with_order(self) -> None:
        node = TransformNode.highpass(frequency=300.0, order=4)
        pb = transform_node_to_proto(node)
        assert pb.highLowPassParams.order == 4

    def test_lowpass(self) -> None:
        node = TransformNode.lowpass(frequency=6000.0)
        pb = transform_node_to_proto(node)
        assert pb.type == common_pb2.LOWPASS_FILTER
        assert pb.highLowPassParams.frequency == 6000.0

    def test_bandpass(self) -> None:
        node = TransformNode.bandpass(low_frequency=300.0, high_frequency=6000.0)
        pb = transform_node_to_proto(node)
        assert pb.type == common_pb2.BANDPASS_FILTER
        assert pb.bandParams.lowFrequency == 300.0
        assert pb.bandParams.highFrequency == 6000.0
        assert not pb.bandParams.HasField("order")

    def test_bandstop(self) -> None:
        node = TransformNode.bandstop(low_frequency=58.0, high_frequency=62.0, order=2)
        pb = transform_node_to_proto(node)
        assert pb.type == common_pb2.BANDSTOP_FILTER
        assert pb.bandParams.lowFrequency == 58.0
        assert pb.bandParams.order == 2

    def test_notch(self) -> None:
        node = TransformNode.notch(notch_frequency=60.0, bandwidth=4.0)
        pb = transform_node_to_proto(node)
        assert pb.type == common_pb2.NOTCH_FILTER
        assert pb.notchParams.notchFrequency == 60.0
        assert pb.notchParams.notchBandwidth == 4.0

    def test_car(self) -> None:
        node = TransformNode.car()
        pb = transform_node_to_proto(node)
        assert pb.type == common_pb2.CAR_REF
        assert pb.HasField("carParams")

    def test_virtual_ref(self) -> None:
        node = TransformNode.virtual_ref(ref_channel=3)
        pb = transform_node_to_proto(node)
        assert pb.type == common_pb2.VIRTUAL_REF
        assert pb.virtualRefParams.refNtvChanIdx == 3

    def test_paired_ref(self) -> None:
        node = TransformNode.paired_ref(ref_channel=1, target_channel=4)
        pb = transform_node_to_proto(node)
        assert pb.type == common_pb2.PAIRED_REF
        assert pb.pairedRefParams.refNtvChanIdx == 1
        assert pb.pairedRefParams.targetNtvChanIdx == 4

    def test_slice_time(self) -> None:
        node = TransformNode.slice_time(start_sec=1.0, end_sec=5.0)
        pb = transform_node_to_proto(node)
        assert pb.type == common_pb2.SLICE_TIME
        assert pb.sliceTimeParams.timeStart == 1.0
        assert pb.sliceTimeParams.timeEnd == 5.0

    def test_slice_channels(self) -> None:
        node = TransformNode.slice_channels(channels=[0, 2, 5])
        pb = transform_node_to_proto(node)
        assert pb.type == common_pb2.SLICE_CHANNELS
        assert list(pb.sliceChannelsParams.sysChanIdxs) == [0, 2, 5]

    def test_downsample(self) -> None:
        node = TransformNode.downsample(factor=4)
        pb = transform_node_to_proto(node)
        assert pb.type == common_pb2.DOWNSAMPLE
        assert pb.downsampleParams.sampleFactor == 4

    def test_source_params(self) -> None:
        node = TransformNode(
            id="src1",
            type=TransformNodeType.SOURCE,
            params=SourceParams(
                dataset_id="ds42",
                base_name="recording",
                path="/data",
                file_type=RadiensFileType.XDAT,
            ),
        )
        pb = transform_node_to_proto(node)
        assert pb.type == common_pb2.SOURCE
        assert pb.datasourceSinkParams.id == "ds42"
        assert pb.datasourceSinkParams.dsrcName == "recording"
        assert pb.datasourceSinkParams.path == "/data"

    def test_sink_params(self) -> None:
        node = TransformNode(
            id="snk1",
            type=TransformNodeType.SINK,
            params=SinkParams(base_name="out", path="/output", file_type=RadiensFileType.XDAT),
        )
        pb = transform_node_to_proto(node)
        assert pb.type == common_pb2.SINK
        assert pb.datasourceSinkParams.dsrcName == "out"
        assert pb.datasourceSinkParams.path == "/output"
        # SINK has no dataset id
        assert pb.datasourceSinkParams.id == ""

    def test_bulk_source(self) -> None:
        node = TransformNode(
            id="bs1",
            type=TransformNodeType.BULK_SOURCE,
            params=BulkSourceParams(
                sources=[
                    SourceParams(dataset_id="d0", base_name="r0", path="/p0", file_type=RadiensFileType.XDAT),
                    SourceParams(dataset_id="d1", base_name="r1", path="/p1", file_type=RadiensFileType.XDAT),
                ]
            ),
        )
        pb = transform_node_to_proto(node)
        assert pb.type == common_pb2.BULK_SOURCE
        src_params = list(pb.bulkDataSourceParams.srcParams)
        assert len(src_params) == 2
        assert src_params[0].id == "d0"
        assert src_params[1].id == "d1"

    def test_bulk_sink(self) -> None:
        node = TransformNode(
            id="bsnk1",
            type=TransformNodeType.BULK_SINK,
            params=BulkSinkParams(suffix="_hp", path="/out", file_type=RadiensFileType.XDAT),
        )
        pb = transform_node_to_proto(node)
        assert pb.type == common_pb2.BULK_SINK
        assert pb.bulkDataSinkParams.suffix == "_hp"
        assert pb.bulkDataSinkParams.path == "/out"

    def test_node_id_preserved(self) -> None:
        node = TransformNode(id="fixed-id", type=TransformNodeType.CAR_REF, params=CARParams())
        pb = transform_node_to_proto(node)
        assert pb.id == "fixed-id"

    def test_none_params_node(self) -> None:
        """Node with no params (e.g., REMAP_SENSOR placeholder) converts without error."""
        node = TransformNode(id="remap1", type=TransformNodeType.REMAP_SENSOR)
        pb = transform_node_to_proto(node)
        assert pb.type == common_pb2.REMAP_SENSOR
        assert pb.id == "remap1"


class TestProtocolToProto:
    def test_basic_linear_protocol(self) -> None:
        src = TransformNode(
            id="src",
            type=TransformNodeType.SOURCE,
            params=SourceParams(dataset_id="ds1", base_name="rec", path="/data", file_type=RadiensFileType.XDAT),
        )
        hp = TransformNode.highpass(frequency=300.0)
        hp = TransformNode(id="hp", type=TransformNodeType.HIGHPASS_FILTER, params=HighLowPassParams(frequency=300.0))
        snk = TransformNode(
            id="snk",
            type=TransformNodeType.SINK,
            params=SinkParams(base_name="out", path="/output", file_type=RadiensFileType.XDAT),
        )
        edges = [
            TransformEdge(id="e1", source="src", target="hp"),
            TransformEdge(id="e2", source="hp", target="snk"),
        ]

        proto = protocol_to_proto("proto-1", [src, hp, snk], edges)

        assert proto.id == "proto-1"
        assert len(proto.transformNodes) == 3
        assert len(proto.transformEdges) == 2
        assert proto.transformNodes[0].id == "src"
        assert proto.transformNodes[1].id == "hp"
        assert proto.transformNodes[2].id == "snk"
        assert proto.transformEdges[0].source == "src"
        assert proto.transformEdges[0].target == "hp"
        assert proto.transformEdges[1].source == "hp"
        assert proto.transformEdges[1].target == "snk"


class TestTransformNodeFactories:
    def test_auto_uuid_id(self) -> None:
        n1 = TransformNode.car()
        n2 = TransformNode.car()
        assert n1.id != n2.id  # each call generates a unique ID

    def test_source_from_metadata(self) -> None:
        from unittest.mock import MagicMock

        meta = MagicMock()
        meta.id = "ds99"
        meta.base_name = "myfile"
        meta.path = "/recordings"
        meta.file_type = RadiensFileType.XDAT

        node = TransformNode.source(meta)
        assert node.type == TransformNodeType.SOURCE
        assert isinstance(node.params, SourceParams)
        assert node.params.dataset_id == "ds99"
        assert node.params.base_name == "myfile"

    def test_bulk_source_from_metadata_list(self) -> None:
        from unittest.mock import MagicMock

        metas = []
        for i in range(3):
            m = MagicMock()
            m.id = f"ds{i}"
            m.base_name = f"file{i}"
            m.path = "/data"
            m.file_type = RadiensFileType.XDAT
            metas.append(m)

        node = TransformNode.bulk_source(metas)
        assert node.type == TransformNodeType.BULK_SOURCE
        assert isinstance(node.params, BulkSourceParams)
        assert len(node.params.sources) == 3
        assert node.params.sources[0].dataset_id == "ds0"
