"""
Tests for the map-matching module (walkingpandas.io.match).

General-purpose tests applicable to any BaseMatcher backend.  All tests
use ``MagicMock`` backends or shared infrastructure and do **not** import
anything from ``walkingpandas.io.match.backends.valhalla``.
"""

from unittest.mock import MagicMock

import pandas as pd
import pytest

from walkingpandas.io.match.osm_utils import generate_cache_key
from walkingpandas.io.match.gps import GPSDataset
from walkingpandas.io.match.core import match_to_disk, match_trajectories


# ===================================================================
# Shared infrastructure tests
# ===================================================================


class TestGenerateCacheKey:
    def test_deterministic(self):
        k1 = generate_cache_key(bbox=(47.4, 47.3, 8.6, 8.5))
        k2 = generate_cache_key(bbox=(47.4, 47.3, 8.6, 8.5))
        assert k1 == k2

    def test_different_bbox(self):
        k1 = generate_cache_key(bbox=(47.4, 47.3, 8.6, 8.5))
        k2 = generate_cache_key(bbox=(48.0, 47.0, 9.0, 8.0))
        assert k1 != k2

    def test_place(self):
        k = generate_cache_key(place="Zurich")
        assert isinstance(k, str) and len(k) == 16

    def test_no_args_raises(self):
        with pytest.raises(ValueError):
            generate_cache_key()


# ===================================================================
# GPSDataset tests
# ===================================================================


class TestGPSDataset:
    def test_csv_bbox(self, sample_gps_csv):
        csv_path, raw_df = sample_gps_csv
        ds = GPSDataset(csv_path)
        north, south, east, west = ds.estimate_bbox()

        assert north == pytest.approx(raw_df["lat"].max())
        assert south == pytest.approx(raw_df["lat"].min())
        assert east == pytest.approx(raw_df["lon"].max())
        assert west == pytest.approx(raw_df["lon"].min())

    def test_csv_unique_trips(self, sample_gps_csv):
        csv_path, _ = sample_gps_csv
        ds = GPSDataset(csv_path)
        trips = ds.get_unique_trip_ids()
        assert set(trips) == {"trip_1", "trip_2"}

    def test_csv_load_trips(self, sample_gps_csv):
        csv_path, raw_df = sample_gps_csv
        ds = GPSDataset(csv_path)
        loaded = ds.load_trips(["trip_1"])

        assert set(loaded.columns) >= {"trip_id", "timestamp", "lon", "lat"}
        assert len(loaded) == len(raw_df[raw_df["trip_id"] == "trip_1"])

    def test_parquet(self, temp_dir, sample_gps_csv):
        _, raw_df = sample_gps_csv
        pq_path = temp_dir / "gps.parquet"
        raw_df.to_parquet(pq_path, index=False)

        ds = GPSDataset(pq_path)
        assert set(ds.get_unique_trip_ids()) == {"trip_1", "trip_2"}

    def test_load_empty_trips(self, sample_gps_csv):
        csv_path, _ = sample_gps_csv
        ds = GPSDataset(csv_path)
        empty = ds.load_trips([])
        assert empty.empty

    def test_empty_path_raises(self):
        with pytest.raises(ValueError, match="at least one path"):
            GPSDataset([])


# ===================================================================
# match_to_disk / match_trajectories tests (io/match/core.py)
# ===================================================================


class TestMatchToDisk:
    def test_happy_path(self, temp_dir, sample_gps_csv, sample_nodes, sample_edges):
        csv_path, _ = sample_gps_csv
        _, nodes_path = sample_nodes
        _, edges_path = sample_edges

        from walkingpandas import Network

        network = Network(nodes=nodes_path, edges=edges_path)
        dataset = GPSDataset(csv_path)

        backend = MagicMock()
        backend.trace.return_value = pd.DataFrame([
            {"trip_id": "trip_1", "raw_shape": "", "valhalla_edges": [], "matched_points": []},
        ])
        enriched = pd.DataFrame({
            "walk_id": ["trip_1", "trip_1"],
            "sequence_idx": [0, 1],
            "node_id": pd.array([0, 1], dtype=pd.Int64Dtype()),
            "exit_edge": pd.array([0, None], dtype=pd.Int64Dtype()),
        })
        backend.snap.return_value = enriched
        backend.enrich.return_value = enriched

        out_dir = temp_dir / "matched"
        wf = match_to_disk(dataset, backend, network, out_dir, chunk_size_trips=10)

        assert wf is not None
        backend.trace.assert_called()
        backend.snap.assert_called()

    def test_empty_match_raises(self, temp_dir, sample_gps_csv, sample_nodes, sample_edges):
        csv_path, _ = sample_gps_csv
        _, nodes_path = sample_nodes
        _, edges_path = sample_edges

        from walkingpandas import Network

        network = Network(nodes=nodes_path, edges=edges_path)
        dataset = GPSDataset(csv_path)

        backend = MagicMock()
        backend.trace.return_value = pd.DataFrame()
        backend.snap.return_value = pd.DataFrame()
        backend.enrich.return_value = pd.DataFrame()

        with pytest.raises(ValueError, match="No trajectories"):
            match_to_disk(dataset, backend, network, temp_dir / "out", chunk_size_trips=10)


class TestMatchTrajectories:
    def test_happy_path(self, temp_dir, sample_gps_csv, sample_nodes, sample_edges):
        csv_path, _ = sample_gps_csv
        _, nodes_path = sample_nodes
        _, edges_path = sample_edges

        from walkingpandas import Network

        network = Network(nodes=nodes_path, edges=edges_path)

        backend = MagicMock()
        backend.region = (47.4, 47.3, 8.6, 8.5)
        backend.get_network.return_value = network
        enriched = pd.DataFrame({
            "walk_id": ["trip_1", "trip_1"],
            "sequence_idx": [0, 1],
            "node_id": pd.array([0, 1], dtype=pd.Int64Dtype()),
            "exit_edge": pd.array([0, None], dtype=pd.Int64Dtype()),
        })
        backend.trace.return_value = pd.DataFrame([
            {"trip_id": "trip_1", "raw_shape": "", "valhalla_edges": [], "matched_points": []},
        ])
        backend.snap.return_value = enriched
        backend.enrich.return_value = enriched

        wf = match_trajectories(
            gps_data=str(csv_path),
            backend=backend,
            output_dir=str(temp_dir / "out"),
            network=network,
        )
        assert wf is not None

    def test_auto_region_and_network(self, temp_dir, sample_gps_csv, sample_nodes, sample_edges):
        csv_path, _ = sample_gps_csv
        _, nodes_path = sample_nodes
        _, edges_path = sample_edges

        from walkingpandas import Network

        network = Network(nodes=nodes_path, edges=edges_path)

        backend = MagicMock()
        backend.region = None
        backend.get_network.return_value = network
        enriched = pd.DataFrame({
            "walk_id": ["trip_1", "trip_1"],
            "sequence_idx": [0, 1],
            "node_id": pd.array([0, 1], dtype=pd.Int64Dtype()),
            "exit_edge": pd.array([0, None], dtype=pd.Int64Dtype()),
        })
        backend.trace.return_value = pd.DataFrame([
            {"trip_id": "trip_1", "raw_shape": "", "valhalla_edges": [], "matched_points": []},
        ])
        backend.snap.return_value = enriched
        backend.enrich.return_value = enriched

        wf = match_trajectories(
            gps_data=str(csv_path),
            backend=backend,
            output_dir=str(temp_dir / "out"),
        )
        backend.set_region.assert_called_once()
        backend.get_network.assert_called_once()
