"""Tests for WalkFrame class."""

import pytest
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point, LineString
from walkingpandas import WalkFrame, Network


def test_walkframe_initialization(sample_walkframe):
    """Test WalkFrame initialization."""
    assert sample_walkframe is not None
    assert sample_walkframe._network is not None


def test_walkframe_network_property(sample_walkframe):
    """Test that .network returns the underlying Network."""
    net = sample_walkframe.network
    assert net is sample_walkframe._network
    nodes = net.get_nodes(as_geopandas=False)
    assert len(nodes) > 0


def test_walkframe_with_nodes_and_edges(sample_walkframe, sample_walks_simple_paths, sample_nodes, sample_edges):
    """Test WalkFrame(parquet_path, nodes=..., edges=...) builds network internally."""
    _, walks_path = sample_walks_simple_paths
    nodes_gdf, _ = sample_nodes
    edges_gdf, _ = sample_edges
    wf = WalkFrame(walks_path, nodes=nodes_gdf, edges=edges_gdf)
    result = wf.compute()
    assert len(result) > 0
    assert wf.network is not None
    assert len(wf.network.get_nodes(as_geopandas=False)) == len(nodes_gdf)
    filtered = wf.passing_through(0).compute()
    assert "node_id" in filtered.columns


def test_walkframe_with_nodes_only(sample_walks_simple_paths, sample_nodes):
    """Test WalkFrame(parquet_path, nodes=...) with only nodes (no edges)."""
    _, walks_path = sample_walks_simple_paths
    nodes_gdf, _ = sample_nodes
    wf = WalkFrame(walks_path, nodes=nodes_gdf)
    result = wf.compute()
    assert len(result) > 0
    assert wf.network.get_nodes(as_geopandas=False) is not None


def test_walkframe_from_walks_infer_network(sample_walks_simple_paths):
    """Test WalkFrame.from_walks(parquet_path, infer_network=True)."""
    _, walks_path = sample_walks_simple_paths
    wf = WalkFrame.from_walks(walks_path, infer_network=True)
    result = wf.compute()
    assert len(result) > 0
    assert wf.network is not None
    nodes = wf.network.get_nodes(as_geopandas=False)
    assert len(nodes) >= 2


def test_walkframe_from_walks_with_network(sample_walkframe, sample_walks_simple_paths):
    """Test WalkFrame.from_walks(parquet_path, network=net)."""
    _, walks_path = sample_walks_simple_paths
    net = sample_walkframe.network
    wf = WalkFrame.from_walks(walks_path, network=net)
    result = wf.compute()
    assert len(result) > 0
    assert wf.network is net


def test_walkframe_network_and_nodes_raises(sample_walkframe, sample_walks_simple_paths, sample_nodes):
    """Test that providing both network= and nodes= raises."""
    _, walks_path = sample_walks_simple_paths
    nodes_gdf, _ = sample_nodes
    with pytest.raises(ValueError, match="either network= or"):
        WalkFrame(walks_path, network=sample_walkframe.network, nodes=nodes_gdf)


def test_walkframe_from_walks_requires_network_or_infer(sample_walks_simple_paths):
    """Test WalkFrame.from_walks() without network= or infer_network=True raises."""
    _, walks_path = sample_walks_simple_paths
    with pytest.raises(ValueError, match="network= or infer_network"):
        WalkFrame.from_walks(walks_path)


def test_walkframe_from_parquet(sample_walkframe, sample_walks_simple_paths):
    """Test WalkFrame.from_parquet delegates to constructor."""
    _, walks_path = sample_walks_simple_paths
    net = sample_walkframe.network
    wf = WalkFrame.from_parquet(walks_path, network=net)
    result = wf.compute()
    assert len(result) > 0
    assert wf.network is net


def test_walkframe_from_parquet_with_nodes_edges(sample_walks_simple_paths, sample_nodes, sample_edges):
    """Test WalkFrame.from_parquet with nodes= and edges= builds network."""
    _, walks_path = sample_walks_simple_paths
    nodes_gdf, _ = sample_nodes
    edges_gdf, _ = sample_edges
    wf = WalkFrame.from_parquet(walks_path, nodes=nodes_gdf, edges=edges_gdf)
    result = wf.compute()
    assert len(result) > 0
    assert wf.network is not None


def test_walkframe_from_duckdb(sample_nodes, sample_edges, sample_walks_simple_paths, temp_dir):
    """Test WalkFrame.from_duckdb loads from single file."""
    from walkingpandas.io.ingest import write_duckdb
    _, nodes_path = sample_nodes
    _, edges_path = sample_edges
    walks_df, _ = sample_walks_simple_paths
    network = Network(nodes=nodes_path, edges=edges_path)
    db_path = temp_dir / "from_duckdb.duckdb"
    write_duckdb(network, walks_df, db_path)
    wf = WalkFrame.from_duckdb(db_path)
    result = wf.compute()
    assert len(result) == len(walks_df)
    assert set(result.columns) >= {'walk_id', 'sequence_idx', 'node_id'}


def test_walkframe_from_dataframe_with_network(sample_walkframe, sample_walks_simple_paths):
    """Test WalkFrame.from_dataframe with network=."""
    walks_df, _ = sample_walks_simple_paths
    net = sample_walkframe.network
    wf = WalkFrame.from_dataframe(walks_df, network=net)
    result = wf.compute()
    assert len(result) == len(walks_df)


def test_walkframe_from_dataframe_with_nodes_edges(sample_walks_simple_paths, sample_nodes, sample_edges):
    """Test WalkFrame.from_dataframe with nodes= and edges=."""
    walks_df, _ = sample_walks_simple_paths
    nodes_gdf, _ = sample_nodes
    edges_gdf, _ = sample_edges
    wf = WalkFrame.from_dataframe(walks_df, nodes=nodes_gdf, edges=edges_gdf)
    result = wf.compute()
    assert len(result) == len(walks_df)
    assert wf.network is not None


def test_walkframe_from_dataframe_convert_schema(sample_nodes, sample_edges):
    """Test WalkFrame.from_dataframe converts id/step/node to superset schema."""
    nodes_gdf, _ = sample_nodes
    edges_gdf, _ = sample_edges
    df = pd.DataFrame({'id': [1, 1], 'step': [0, 1], 'node': [0, 1]})
    wf = WalkFrame.from_dataframe(df, nodes=nodes_gdf, edges=edges_gdf)
    result = wf.compute()
    assert 'walk_id' in result.columns
    assert 'sequence_idx' in result.columns
    assert 'node_id' in result.columns
    assert len(result) == 2


def test_walkframe_compute(sample_walkframe):
    """Test basic compute() execution."""
    result = sample_walkframe.compute()
    assert isinstance(result, pd.DataFrame)
    assert len(result) > 0


def test_walkframe_filter(sample_walkframe):
    """Test filtering functionality."""
    filtered = sample_walkframe.filter(walk_id='walk1')
    result = filtered.compute()
    assert all(result['walk_id'] == 'walk1')


def test_walkframe_limit(sample_walkframe):
    """Test limit functionality."""
    limited = sample_walkframe.limit(2)
    result = limited.compute()
    assert len(result) <= 2


def test_walkframe_get_orphaned_records(sample_network, sample_walks_orphaned, temp_dir):
    """Test orphaned records detection."""
    _, walks_path = sample_walks_orphaned
    walkframe = WalkFrame(walks_path, network=sample_network)

    orphans = walkframe.get_orphaned_records()
    assert isinstance(orphans, pd.DataFrame)
    assert len(orphans) > 0


def test_walkframe_to_pandas(sample_walkframe):
    """Test to_pandas() alias for compute()."""
    result = sample_walkframe.to_pandas()
    assert isinstance(result, pd.DataFrame)
    assert len(result) > 0


def test_walkframe_to_geopandas(sample_walkframe):
    """Test to_geopandas() returns GeoDataFrame."""
    result = sample_walkframe.to_geopandas()
    assert isinstance(result, gpd.GeoDataFrame)
    assert len(result) > 0


def test_walkframe_get_orphaned_records_empty(sample_walkframe):
    """Test get_orphaned_records when no orphans returns empty DataFrame."""
    orphans = sample_walkframe.get_orphaned_records()
    assert isinstance(orphans, pd.DataFrame)
    assert len(orphans) == 0


def test_walkframe_filter_with_kwargs(sample_walkframe):
    """Test filter with column kwargs (e.g. node_id as integer)."""
    filtered = sample_walkframe.filter(node_id=0)
    result = filtered.compute()
    assert all(result['node_id'] == 0)


def test_walkframe_filter_with_list_value(sample_walkframe):
    """Test filter with list value (IN clause)."""
    filtered = sample_walkframe.filter(node_id=[0, 1])
    result = filtered.compute()
    assert all(result['node_id'].isin([0, 1]))


def test_walkframe_order_by_in_build_sql(sample_walkframe):
    """Test that order_by in query state is reflected in SQL."""
    sample_walkframe._query_state['order_by'] = 'sequence_idx'
    result = sample_walkframe.compute()
    assert isinstance(result, pd.DataFrame)
    assert len(result) >= 0


def test_walkframe_branching(sample_walkframe):
    """Test that chaining from a base query branches: two branches stay independent."""
    base = sample_walkframe.only_simple_paths()
    query_a = base.passing_through(0)
    query_b = base.passing_through(1)
    assert query_a is not query_b
    assert query_a is not base
    result_a = query_a.compute()
    result_b = query_b.compute()
    assert "node_id" in result_a.columns
    assert "node_id" in result_b.columns
    if len(result_a) > 0:
        assert 0 in result_a["node_id"].values
    if len(result_b) > 0:
        assert 1 in result_b["node_id"].values
    base_result = base.compute()
    assert isinstance(base_result, pd.DataFrame)


# -------------------------------------------------------------------------
# Phase 4: String translation tests
# -------------------------------------------------------------------------


def test_walkframe_string_ids_end_to_end(temp_dir):
    """Test the full string-ID workflow: string in, string out."""
    network = Network()
    network.add_nodes(pd.DataFrame({"node_id": ["Station_A", "Station_B", "Station_C"]}))
    network.add_edges(pd.DataFrame({
        "edge_id": ["link_AB", "link_BC"],
        "source_node": ["Station_A", "Station_B"],
        "target_node": ["Station_B", "Station_C"],
    }))
    assert network.has_string_mapping

    walks_df = pd.DataFrame({
        "walk_id": ["w1", "w1", "w1"],
        "sequence_idx": [0, 1, 2],
        "node_id": ["Station_A", "Station_B", "Station_C"],
        "exit_edge": ["link_AB", "link_BC", None],
    })
    wf = WalkFrame.from_dataframe(walks_df, network=network)
    result = wf.compute(decode=True)
    assert set(result["node_id"]) == {"Station_A", "Station_B", "Station_C"}

    filtered = wf.passing_through("Station_B").compute(decode=True)
    assert len(filtered) > 0
    assert "Station_B" in filtered["node_id"].values

    raw = wf.compute(decode=False)
    assert raw["node_id"].dtype in ('int64', pd.Int64Dtype())


def test_walkframe_no_source_raises(sample_network):
    """Test WalkFrame with neither parquet_path nor walks_table raises."""
    with pytest.raises(ValueError, match="parquet_path or walks_table"):
        WalkFrame(None, network=sample_network)


def test_walkframe_both_sources_raises(sample_walkframe, sample_walks_simple_paths):
    """Test WalkFrame with both parquet_path and walks_table raises."""
    _, walks_path = sample_walks_simple_paths
    with pytest.raises(ValueError, match="exactly one"):
        WalkFrame(walks_path, network=sample_walkframe.network, walks_table="some_table")


def test_walkframe_no_network_no_nodes_raises(sample_walks_simple_paths):
    """Test WalkFrame without network= or nodes= raises."""
    _, walks_path = sample_walks_simple_paths
    with pytest.raises(ValueError, match="network= or nodes="):
        WalkFrame(walks_path)


def test_walkframe_from_dataframe_both_network_and_nodes_raises(
    sample_walkframe, sample_walks_simple_paths, sample_nodes
):
    """Test from_dataframe with both network= and nodes= raises."""
    walks_df, _ = sample_walks_simple_paths
    nodes_gdf, _ = sample_nodes
    with pytest.raises(ValueError, match="either network= or"):
        WalkFrame.from_dataframe(walks_df, network=sample_walkframe.network, nodes=nodes_gdf)


def test_get_orphaned_nodes_only(sample_network, temp_dir):
    """Test get_orphaned_records when only orphan nodes exist (not edges)."""
    walks_data = [
        {"walk_id": "w1", "sequence_idx": 0, "node_id": 99, "exit_edge": None},
        {"walk_id": "w1", "sequence_idx": 1, "node_id": 0, "exit_edge": None},
    ]
    df = pd.DataFrame(walks_data)
    df["node_id"] = df["node_id"].astype(pd.Int64Dtype())
    df["exit_edge"] = df["exit_edge"].astype(pd.Int64Dtype())
    path = temp_dir / "orphan_nodes.parquet"
    df.to_parquet(path, index=False)

    wf = WalkFrame(path, network=sample_network)
    orphans = wf.get_orphaned_records()
    assert len(orphans) >= 1
    assert 99 in orphans["node_id"].values


def test_get_orphaned_edges_only(sample_network, temp_dir):
    """Test get_orphaned_records when only orphan edges exist."""
    walks_data = [
        {"walk_id": "w1", "sequence_idx": 0, "node_id": 0, "exit_edge": 999},
        {"walk_id": "w1", "sequence_idx": 1, "node_id": 1, "exit_edge": None},
    ]
    df = pd.DataFrame(walks_data)
    df["node_id"] = df["node_id"].astype(pd.Int64Dtype())
    df["exit_edge"] = df["exit_edge"].astype(pd.Int64Dtype())
    path = temp_dir / "orphan_edges.parquet"
    df.to_parquet(path, index=False)

    wf = WalkFrame(path, network=sample_network)
    orphans = wf.get_orphaned_records()
    assert len(orphans) >= 1


def test_walkframe_resolve_filter_value_edge_id(temp_dir):
    """Test _resolve_filter_value resolves edge string IDs through mapping."""
    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"],
    }))

    walks_df = pd.DataFrame({
        "walk_id": ["w1", "w1", "w1"],
        "sequence_idx": [0, 1, 2],
        "node_id": ["A", "B", "C"],
        "exit_edge": ["e1", "e2", None],
    })
    wf = WalkFrame.from_dataframe(walks_df, network=network)
    resolved = wf._resolve_filter_value("exit_edge", "e1")
    assert isinstance(resolved, int)
    resolved_list = wf._resolve_filter_value("exit_edge", ["e1", "e2"])
    assert all(isinstance(v, int) for v in resolved_list)


def test_walkframe_filter_string_node_id(temp_dir):
    """Test filter(node_id='Station_A') resolves through the mapping."""
    network = Network()
    network.add_nodes(pd.DataFrame({"node_id": ["X", "Y"]}))
    network.add_edges(pd.DataFrame({
        "source_node": ["X"],
        "target_node": ["Y"],
    }))

    walks_df = pd.DataFrame({
        "walk_id": ["w1", "w1"],
        "sequence_idx": [0, 1],
        "node_id": ["X", "Y"],
    })
    wf = WalkFrame.from_dataframe(walks_df, network=network)
    filtered = wf.filter(node_id="X").compute(decode=True)
    assert len(filtered) == 1
    assert filtered["node_id"].iloc[0] == "X"
