"""Tests for Network class."""

import pytest
import pandas as pd
import geopandas as gpd
from pathlib import Path
from unittest.mock import patch
from walkingpandas import Network


def test_network_initialization(sample_network, sample_nodes, sample_edges):
    """Test Network initialization from Parquet files."""
    assert sample_network is not None
    assert sample_network.connection is not None


def test_network_from_gdfs(sample_nodes, sample_edges):
    """Test Network creation from GeoDataFrames."""
    nodes_gdf, _ = sample_nodes
    edges_gdf, _ = sample_edges

    network = Network.from_gdfs(nodes_gdf, edges_gdf)
    assert network is not None


def test_network_get_nodes(sample_network):
    """Test getting nodes from Network."""
    nodes = sample_network.get_nodes()
    assert len(nodes) > 0
    assert 'node_id' in nodes.columns
    assert isinstance(nodes, gpd.GeoDataFrame)
    assert 'geometry' in nodes.columns or len(nodes) == 0


def test_network_get_nodes_as_dataframe(sample_network):
    """Test getting nodes as DataFrame (not GeoDataFrame)."""
    nodes = sample_network.get_nodes(as_geopandas=False)
    assert len(nodes) > 0
    assert 'node_id' in nodes.columns
    assert not isinstance(nodes, gpd.GeoDataFrame)


def test_network_get_edges(sample_network):
    """Test getting edges from Network."""
    edges = sample_network.get_edges()
    assert len(edges) > 0
    assert 'edge_id' in edges.columns
    assert isinstance(edges, gpd.GeoDataFrame)
    assert 'geometry' in edges.columns or len(edges) == 0


def test_network_get_edges_as_dataframe(sample_network):
    """Test getting edges as DataFrame (not GeoDataFrame)."""
    edges = sample_network.get_edges(as_geopandas=False)
    assert len(edges) > 0
    assert 'edge_id' in edges.columns
    assert not isinstance(edges, gpd.GeoDataFrame)


def test_network_save_and_load(sample_network, temp_dir):
    """Test saving and loading Network to/from DuckDB file."""
    db_path = temp_dir / 'test_network.duckdb'

    sample_network.save(db_path)
    assert db_path.exists()

    network_loaded = Network.load(db_path)
    assert network_loaded is not None

    nodes_original = sample_network.get_nodes()
    nodes_loaded = network_loaded.get_nodes()

    assert len(nodes_original) == len(nodes_loaded)
    assert set(nodes_original['node_id']) == set(nodes_loaded['node_id'])

    edges_original = sample_network.get_edges()
    edges_loaded = network_loaded.get_edges()

    assert len(edges_original) == len(edges_loaded)
    assert set(edges_original['edge_id']) == set(edges_loaded['edge_id'])


def test_network_with_persistent_database(sample_nodes, sample_edges, temp_dir):
    """Test Network initialization with persistent database."""
    nodes_gdf, _ = sample_nodes
    edges_gdf, _ = sample_edges

    db_path = temp_dir / 'persistent_network.duckdb'

    network = Network(nodes=nodes_gdf, edges=edges_gdf, database=db_path)
    assert db_path.exists()

    nodes = network.get_nodes()
    assert len(nodes) > 0


def test_network_load_nonexistent_file(temp_dir):
    """Test loading non-existent database file raises error."""
    db_path = temp_dir / 'nonexistent.duckdb'

    with pytest.raises(FileNotFoundError):
        Network.load(db_path)


def test_network_add_nodes_from_dataframe_with_xy(temp_dir):
    """Test add_nodes with DataFrame that has x/y coordinates (integer IDs)."""
    nodes_df = pd.DataFrame([
        {'node_id': 10, 'x': 0, 'y': 0},
        {'node_id': 20, 'x': 1, 'y': 1},
    ])
    network = Network()
    network.add_nodes(nodes_df)
    nodes = network.get_nodes()
    assert len(nodes) == 2
    assert set(nodes['node_id']) == {10, 20}


def test_network_add_nodes_missing_node_id_raises():
    """Test add_nodes with missing node_id raises ValueError."""
    nodes_df = pd.DataFrame([{'x': 0, 'y': 0}])
    network = Network()
    with pytest.raises(ValueError, match="node_id"):
        network.add_nodes(nodes_df)


def test_network_add_nodes_unsupported_type_raises(sample_network):
    """Test add_nodes with unsupported type raises TypeError."""
    with pytest.raises(TypeError, match="Unsupported"):
        sample_network.add_nodes([1, 2, 3])


def test_network_add_edges_unsupported_type_raises(sample_network):
    """Test add_edges with unsupported type raises TypeError."""
    with pytest.raises(TypeError, match="Unsupported"):
        sample_network.add_edges([1, 2, 3])


def test_network_from_networkx(sample_nodes, sample_edges):
    """Test Network creation from NetworkX graph."""
    pytest.importorskip("networkx")
    import networkx as nx

    G = nx.DiGraph()
    G.add_node('A', x=0, y=0)
    G.add_node('B', x=1, y=0)
    G.add_node('C', x=1, y=1)
    G.add_edge('A', 'B', edge_id='AB')
    G.add_edge('B', 'C', edge_id='BC')

    network = Network.from_networkx(G)
    assert network is not None
    nodes = network.get_nodes(as_geopandas=False)
    edges = network.get_edges(as_geopandas=False)
    assert len(nodes) == 3
    assert len(edges) == 2


def test_network_from_networkx_with_geometry(sample_nodes):
    """Test from_networkx when nodes have geometry attribute."""
    pytest.importorskip("networkx")
    import networkx as nx
    from shapely.geometry import Point

    G = nx.DiGraph()
    G.add_node('A', geometry=Point(0, 0))
    G.add_node('B', geometry=Point(1, 0))
    G.add_edge('A', 'B')

    network = Network.from_networkx(G)
    assert network is not None
    nodes = network.get_nodes()
    assert len(nodes) == 2


def test_network_get_nodes_no_geometry(temp_dir):
    """Test get_nodes when geometry is all NULL (empty geom_wkt)."""
    network = Network()
    network._conn.execute("INSERT INTO wp_nodes (node_id, geom, attributes) VALUES (100, NULL, NULL)")
    nodes = network.get_nodes()
    assert len(nodes) == 1
    assert nodes['node_id'].iloc[0] == 100


def test_network_add_nodes_node_id_only():
    """Test add_nodes with only node_id (no geometry) for topology-only networks."""
    network = Network()
    network.add_nodes(pd.DataFrame({"node_id": [10, 20, 30]}))
    nodes = network.get_nodes(as_geopandas=False)
    assert len(nodes) == 3
    assert set(nodes["node_id"]) == {10, 20, 30}


def test_network_add_nodes_string_auto_maps():
    """Test add_nodes with string IDs triggers auto-mapping."""
    network = Network()
    network.add_nodes(pd.DataFrame({"node_id": ["A", "B", "C"]}))
    assert network.has_string_mapping
    nodes = network.get_nodes(as_geopandas=False, decode=True)
    assert set(nodes["node_id"]) == {"A", "B", "C"}
    nodes_raw = network.get_nodes(as_geopandas=False, decode=False)
    assert nodes_raw["node_id"].dtype in ('int64', pd.Int64Dtype())


def test_network_from_walks_parquet(sample_walks_simple_paths):
    """Test Network.from_walks with Parquet path infers nodes and edges."""
    _, walks_path = sample_walks_simple_paths
    network = Network.from_walks(walks_path)
    nodes = network.get_nodes(as_geopandas=False)
    edges = network.get_edges(as_geopandas=False)
    assert len(nodes) == 4  # 0, 1, 2, 4
    assert set(nodes["node_id"]) == {0, 1, 2, 4}
    assert len(edges) >= 2
    assert "edge_id" in edges.columns and "source_node" in edges.columns


def test_network_from_walks_dataframe(sample_walks_simple_paths):
    """Test Network.from_walks with DataFrame infers nodes and edges."""
    walks_df, _ = sample_walks_simple_paths
    network = Network.from_walks(walks_df)
    nodes = network.get_nodes(as_geopandas=False)
    edges = network.get_edges(as_geopandas=False)
    assert len(nodes) == 4
    assert set(nodes["node_id"]) == {0, 1, 2, 4}
    assert len(edges) >= 2


def test_network_from_walks_infer_edges_false(sample_walks_simple_paths):
    """Test Network.from_walks with infer_edges=False only infers nodes."""
    _, walks_path = sample_walks_simple_paths
    network = Network.from_walks(walks_path, infer_edges=False)
    nodes = network.get_nodes(as_geopandas=False)
    assert len(nodes) == 4
    edges = network.get_edges(as_geopandas=False)
    assert len(edges) == 0


def test_network_from_osmnx_value_error_when_multiple_sources():
    """from_osmnx raises ValueError when more than one source is provided."""
    pytest.importorskip("osmnx")
    with pytest.raises(ValueError, match="exactly one of"):
        Network.from_osmnx(bbox=(1, 2, 3, 4), place="Berlin")
    with pytest.raises(ValueError, match="exactly one of"):
        Network.from_osmnx(place="Berlin", center_point=(52.5, 13.4), dist=1000)
    with pytest.raises(ValueError, match="exactly one of"):
        Network.from_osmnx()


def test_network_from_osmnx_returns_network():
    """from_osmnx(bbox=...) returns a Network with integer node/edge IDs."""
    pytest.importorskip("osmnx")
    pytest.importorskip("networkx")
    import networkx as nx
    from shapely.geometry import LineString

    G = nx.MultiDiGraph()
    G.add_node(1, x=0.0, y=0.0)
    G.add_node(2, x=1.0, y=0.0)
    G.add_edge(1, 2, 0, geometry=LineString([(0.0, 0.0), (1.0, 0.0)]))

    with patch("osmnx.graph_from_bbox", return_value=G):
        network = Network.from_osmnx(bbox=(0, 0, 0, 0), network_type="walk")
    assert network is not None
    nodes = network.get_nodes(as_geopandas=False)
    edges = network.get_edges(as_geopandas=False)
    assert len(nodes) == 2
    assert len(edges) >= 1
    assert "node_id" in nodes.columns
    assert "edge_id" in edges.columns
    assert "source_node" in edges.columns
    assert "target_node" in edges.columns


def test_network_from_osmnx_raises_without_osmnx():
    """When osmnx is not installed, from_osmnx raises ImportError."""
    try:
        import osmnx  # noqa: F401
    except ImportError:
        with pytest.raises(ImportError, match="osmnx is required"):
            Network.from_osmnx(bbox=(37.78, 37.77, -122.41, -122.43))
    else:
        pytest.skip("osmnx is installed; cannot test ImportError path")


def test_network_auto_generate_edge_id():
    """Test that edges without edge_id get auto-generated dense IDs."""
    network = Network()
    network.add_nodes(pd.DataFrame({"node_id": [0, 1, 2]}))
    edges_df = pd.DataFrame({
        "source_node": [0, 1],
        "target_node": [1, 2],
    })
    network.add_edges(edges_df)
    edges = network.get_edges(as_geopandas=False)
    assert len(edges) == 2
    assert set(edges["edge_id"]) == {0, 1}


def test_network_decode_edge_ids():
    """Test _decode_edge_ids round-trips string-mapped edge and node IDs."""
    network = Network()
    network.add_nodes(pd.DataFrame({"node_id": ["A", "B", "C"]}))
    network.add_edges(pd.DataFrame({
        "edge_id": ["e1", "e2"],
        "source_node": ["A", "B"],
        "target_node": ["B", "C"],
    }))
    assert network.has_string_mapping

    edges = network.get_edges(as_geopandas=False, decode=True)
    assert set(edges["edge_id"]) == {"e1", "e2"}
    assert set(edges["source_node"]) == {"A", "B"}
    assert set(edges["target_node"]) == {"B", "C"}

    edges_raw = network.get_edges(as_geopandas=False, decode=False)
    assert edges_raw["edge_id"].dtype in ("int64", pd.Int64Dtype())


def test_network_get_nodes_no_geometry_fallback():
    """Test get_nodes returns a DataFrame fallback when geometry conversion fails."""
    network = Network()
    network._conn.execute(
        "INSERT INTO wp_nodes (node_id, geom, attributes) VALUES (1, NULL, NULL)"
    )
    result = network.get_nodes(as_geopandas=True)
    assert len(result) == 1
    assert 1 in result["node_id"].values


def test_network_get_edges_no_geometry_fallback():
    """Test get_edges falls back to plain DataFrame when no geometry."""
    network = Network()
    network.add_nodes(pd.DataFrame({"node_id": [0, 1]}))
    network.add_edges(pd.DataFrame({
        "edge_id": [0],
        "source_node": [0],
        "target_node": [1],
    }))
    result = network.get_edges(as_geopandas=False)
    assert len(result) == 1
    assert 0 in result["edge_id"].values


def test_network_add_edges_string_node_mapping():
    """Test add_edges with string node IDs that go through the mapping."""
    network = Network()
    network.add_nodes(pd.DataFrame({"node_id": ["X", "Y", "Z"]}))
    assert network.has_string_mapping
    network.add_edges(pd.DataFrame({
        "source_node": ["X", "Y"],
        "target_node": ["Y", "Z"],
    }))
    edges = network.get_edges(as_geopandas=False, decode=True)
    assert len(edges) == 2
    assert set(edges["source_node"]) == {"X", "Y"}


def test_network_from_walks_auto_edge_id():
    """Test from_walks auto-generates edge_id when exit_edge column is absent."""
    walks_df = pd.DataFrame({
        "walk_id": ["w1", "w1", "w1"],
        "sequence_idx": [0, 1, 2],
        "node_id": pd.array([10, 20, 30], dtype=pd.Int64Dtype()),
    })
    network = Network.from_walks(walks_df)
    edges = network.get_edges(as_geopandas=False)
    assert len(edges) == 2
    assert "edge_id" in edges.columns
    assert set(edges["edge_id"]) == {0, 1}


def test_network_string_mapping_save_load(temp_dir):
    """Test that string mappings survive save/load."""
    network = Network()
    network.add_nodes(pd.DataFrame({"node_id": ["Station_A", "Station_B"]}))
    edges_df = pd.DataFrame({
        "edge_id": ["link_1"],
        "source_node": ["Station_A"],
        "target_node": ["Station_B"],
    })
    network.add_edges(edges_df)
    assert network.has_string_mapping

    db_path = temp_dir / "mapped.duckdb"
    network.save(db_path)

    loaded = Network.load(db_path)
    assert loaded.has_string_mapping
    nodes = loaded.get_nodes(as_geopandas=False, decode=True)
    assert set(nodes["node_id"]) == {"Station_A", "Station_B"}
