import os
import string
import tempfile

import geodatasets
import geopandas as gpd
import numpy as np
import pandas as pd
import pytest
from scipy import sparse

from libpysal import graph, weights


@pytest.mark.network
class TestBase:
    def setup_method(self):
        self.neighbor_dict_int = {0: 1, 1: 2, 2: 5, 3: 4, 4: 5, 5: 8, 6: 7, 7: 8, 8: 7}
        self.weight_dict_int_binary = {
            0: 1,
            1: 1,
            2: 1,
            3: 1,
            4: 1,
            5: 1,
            6: 1,
            7: 1,
            8: 1,
        }
        self.index_int = pd.Index(
            [0, 1, 2, 3, 4, 5, 6, 7, 8],
            dtype="int64",
            name="focal",
        )
        self.neighbor_dict_str = {
            string.ascii_letters[k]: string.ascii_letters[v]
            for k, v in self.neighbor_dict_int.items()
        }
        self.weight_dict_str_binary = {
            string.ascii_letters[k]: v for k, v in self.weight_dict_int_binary.items()
        }
        self.index_str = pd.Index(
            [string.ascii_letters[k] for k in self.index_int],
            dtype="object",
            name="focal",
        )
        self.adjacency_int_binary = pd.Series(
            self.weight_dict_int_binary.values(),
            name="weight",
            index=pd.MultiIndex.from_arrays(
                [self.index_int, self.neighbor_dict_int.values()],
                names=["focal", "neighbor"],
            ),
        )
        self.adjacency_str_binary = pd.Series(
            self.weight_dict_str_binary.values(),
            name="weight",
            index=pd.MultiIndex.from_arrays(
                # list() to allow pandas to coerce the data to its StringDtype
                [list(self.index_str), self.neighbor_dict_str.values()],
                names=["focal", "neighbor"],
            ),
        )

        # one isolate, one self-link
        self.W_dict_int = {
            0: {0: 1, 3: 0.5, 1: 0.5},
            1: {0: 0.3, 4: 0.3, 2: 0.3},
            2: {1: 0.5, 5: 0.5},
            3: {0: 0.3, 6: 0.3, 4: 0.3},
            4: {1: 0.25, 3: 0.25, 7: 0.25, 5: 0.25},
            5: {2: 0.3, 4: 0.3, 8: 0.3},
            6: {3: 0.5, 7: 0.5},
            7: {4: 0.3, 6: 0.3, 8: 0.3},
            8: {5: 0.5, 7: 0.5},
            9: {},
        }
        self.W_dict_str = {
            string.ascii_letters[k]: {
                string.ascii_letters[k_]: v_ for k_, v_ in v.items()
            }
            for k, v in self.W_dict_int.items()
        }
        self.g_int = graph.Graph.from_weights_dict(self.W_dict_int)
        self.g_str = graph.Graph.from_weights_dict(self.W_dict_str)
        rng = np.random.default_rng(seed=0)
        self.letters = np.asarray(list(string.ascii_letters[:26]))
        rng.shuffle(self.letters)
        self.W_dict_str_unordered = {
            self.letters[k]: {self.letters[k_]: v_ for k_, v_ in v.items()}
            for k, v in self.W_dict_int.items()
        }
        self.g_str_unodered = graph.Graph.from_weights_dict(self.W_dict_str_unordered)

        self.nybb = gpd.read_file(geodatasets.get_path("nybb")).set_index("BoroName")
        self.guerry = gpd.read_file(geodatasets.get_path("geoda guerry"))

    def test_init(self):
        g = graph.Graph(self.adjacency_int_binary)
        assert isinstance(g, graph.Graph)
        assert hasattr(g, "_adjacency")
        assert g._adjacency.shape == (9,)
        pd.testing.assert_series_equal(g._adjacency, self.adjacency_int_binary)
        assert hasattr(g, "transformation")
        assert g.transformation == "O"

        g = graph.Graph(self.adjacency_str_binary)
        assert isinstance(g, graph.Graph)
        assert hasattr(g, "_adjacency")
        assert g._adjacency.shape == (9,)
        pd.testing.assert_series_equal(g._adjacency, self.adjacency_str_binary)
        assert hasattr(g, "transformation")
        assert g.transformation == "O"

        with pytest.raises(TypeError, match="The adjacency table needs to be"):
            graph.Graph(self.adjacency_int_binary.values)

        with pytest.raises(ValueError, match="The index of the adjacency table"):
            adj = self.adjacency_int_binary.copy()
            adj.index.names = ["foo", "bar"]
            graph.Graph(adj)

        with pytest.raises(
            ValueError, match="The adjacency needs to be named 'weight'"
        ):
            graph.Graph(self.adjacency_int_binary.rename("foo"))

        with pytest.raises(ValueError, match="The 'weight' needs"):
            graph.Graph(self.adjacency_int_binary.astype(str))

        with pytest.raises(
            ValueError, match="The adjacency table cannot contain missing"
        ):
            adj = self.adjacency_int_binary.copy()
            adj.iloc[0] = np.nan
            graph.Graph(adj)

        with pytest.raises(ValueError, match="'transformation' needs to be"):
            graph.Graph(self.adjacency_int_binary, transformation="foo")

    def test___repr__(self):
        expected = (
            "<Graph of 10 nodes and 25 nonzero edges (2 components, 1 isolate)"
            " indexed by\n [0, 1, 2, 3, 4, ...]>"
        )
        assert repr(self.g_int) == expected

        expected = (
            "<Graph of 10 nodes and 25 nonzero edges (2 components, 1 isolate)"
            " indexed by\n"
            " ['a', 'b', 'c', 'd', 'e', ...]>"
        )
        assert repr(self.g_str) == expected

        nybb = graph.Graph.build_contiguity(self.nybb)
        expected = (
            "<Graph of 5 nodes and 10 nonzero edges (2 components, 1 isolate) "
            "indexed by\n"
            " ['Staten Island', 'Queens', 'Brooklyn', 'Manhattan', 'Bronx']>"
        )
        assert repr(nybb) == expected

        h3 = {
            "821f87fffffffff": ("821fb7fffffffff", "821f97fffffffff"),
            "821fb7fffffffff": (
                "821f87fffffffff",
                "821f97fffffffff",
                "82186ffffffffff",
                "821867fffffffff",
            ),
            "821f97fffffffff": (
                "821f87fffffffff",
                "821fb7fffffffff",
                "823967fffffffff",
                "82396ffffffffff",
                "82186ffffffffff",
            ),
            "823967fffffffff": (
                "821f97fffffffff",
                "82396ffffffffff",
                "82186ffffffffff",
            ),
            "82396ffffffffff": ("821f97fffffffff", "823967fffffffff"),
            "82186ffffffffff": (
                "821fb7fffffffff",
                "821f97fffffffff",
                "823967fffffffff",
                "821867fffffffff",
            ),
            "821867fffffffff": ("821fb7fffffffff", "82186ffffffffff"),
        }
        h3_g = graph.Graph.from_dicts(h3)
        expected = (
            "<Graph of 7 nodes and 22 nonzero edges (1 component, 0 isolates) "
            "indexed by\n"
            " ['821f87fffffffff', '821fb7fffffffff', '821f97fffffffff',"
            " '823967fffffff...]>"
        )
        assert repr(h3_g) == expected

    def test_copy(self):
        g_copy = self.g_str.copy()
        assert g_copy == self.g_str
        g_copy._adjacency.iloc[0] = 2
        assert g_copy != self.g_str

    def test_adjacency(self):
        g = graph.Graph(self.adjacency_int_binary)
        adjacency = g.adjacency
        pd.testing.assert_series_equal(adjacency, self.adjacency_int_binary)

        # ensure copy
        adjacency.iloc[0] = 100
        pd.testing.assert_series_equal(g._adjacency, self.adjacency_int_binary)

    def test_w_roundtrip(self):
        w = self.g_int.to_W()
        pd.testing.assert_series_equal(
            self.g_int._adjacency.sort_index(),
            w.to_adjlist(drop_islands=False)
            .set_index(["focal", "neighbor"])["weight"]
            .sort_index(),
            check_index_type=False,
            check_dtype=False,
        )
        g_roundtripped = graph.Graph.from_W(w)
        assert self.g_int == g_roundtripped
        assert isinstance(w.id_order, list)

        w = self.g_str.to_W()
        pd.testing.assert_series_equal(
            self.g_str._adjacency.sort_index(),
            w.to_adjlist(drop_islands=False)
            .set_index(["focal", "neighbor"])["weight"]
            .sort_index(),
            check_index_type=False,
            check_dtype=False,
        )
        g_roundtripped = graph.Graph.from_W(w)
        assert self.g_str == g_roundtripped

        w = weights.lat2W(3, 3)
        g = graph.Graph.from_W(w)
        pd.testing.assert_series_equal(
            g._adjacency.sort_index(),
            w.to_adjlist().set_index(["focal", "neighbor"])["weight"].sort_index(),
            check_index_type=False,
            check_dtype=False,
        )
        w_exp = g.to_W()
        # assert w.neighbors == w_exp.neighbors
        assert w.weights == w_exp.weights

        w.transform = "r"
        g_rowwise = graph.Graph.from_W(w)
        pd.testing.assert_series_equal(
            g_rowwise._adjacency.sort_index(),
            w.to_adjlist().set_index(["focal", "neighbor"])["weight"].sort_index(),
            check_index_type=False,
            check_dtype=False,
        )
        w_trans = g_rowwise.to_W()
        # assert w.neighbors == w_trans.neighbors
        assert w.weights == w_trans.weights

        diag = weights.fill_diagonal(w)
        g_diag = graph.Graph.from_W(diag)
        pd.testing.assert_series_equal(
            g_diag._adjacency.sort_index(),
            diag.to_adjlist().set_index(["focal", "neighbor"])["weight"].sort_index(),
            check_index_type=False,
            check_dtype=False,
        )
        w_diag = g_diag.to_W()
        # assert diag.neighbors == W_diag.neighbors
        assert diag.weights == w_diag.weights

        w = weights.W(
            neighbors={"a": ["b"], "b": ["a", "c"], "c": ["b"], "d": []},
            silence_warnings=True,
        )
        g_isolate = graph.Graph.from_W(w)
        pd.testing.assert_series_equal(
            g_isolate._adjacency.sort_index(),
            w.to_adjlist().set_index(["focal", "neighbor"])["weight"].sort_index(),
            check_index_type=False,
            check_dtype=False,
        )
        w_isolate = g_isolate.to_W()
        # assert w.neighbors == w_isolate.neighbors
        assert w.weights == w_isolate.weights

        w = self.g_str_unodered.to_W()
        assert w.id_order_set
        np.testing.assert_array_equal(w.id_order, self.letters[:10])

    def test_from_sparse(self):
        row = np.array([0, 0, 1, 2, 3, 3])
        col = np.array([1, 3, 3, 2, 1, 3])
        data = np.array([0.1, 0.5, 0.9, 0, 0.3, 0.1])
        sp = sparse.coo_array((data, (row, col)), shape=(4, 4))
        g = graph.Graph.from_sparse(sp)
        expected = graph.Graph.from_arrays(row, col, data)
        assert g == expected, "sparse constructor does not match arrays constructor"

        g = graph.Graph.from_sparse(sp.tocsr())
        assert g == expected, "csc input does not match coo input"

        g = graph.Graph.from_sparse(sp.tocsc())
        assert g == expected, "csr input does not match coo input"

        ids = ["zero", "one", "two", "three"]

        g_named = graph.Graph.from_sparse(
            sp,
            ids=ids,
        )

        expected = graph.Graph.from_arrays(
            ["zero", "zero", "one", "two", "three", "three"],
            ["one", "three", "three", "two", "one", "three"],
            data,
        )
        assert g_named == expected

        g = graph.Graph.from_sparse(
            sp.tocsr(),
            ids=ids,
        )
        assert g == expected, "sparse csr with ids does not match arrays constructor"

        g = graph.Graph.from_sparse(
            sp.tocsc(),
            ids=ids,
        )
        assert g == (expected), "sparse csr with ids does not match arrays constructor"

        dense = np.array(
            [
                [0, 0, 1, 1, 0],
                [0, 0, 1, 1, 0],
                [0, 1, 0, 1, 0],
                [0, 1, 0, 0, 1],
                [0, 1, 0, 1, 0],
            ]
        )
        sp = sparse.csr_array(dense)
        g = graph.Graph.from_sparse(
            sp, ids=["staten_island", "queens", "brooklyn", "manhattan", "bronx"]
        )
        expected = graph.Graph.from_arrays(
            [
                "staten_island",
                "staten_island",
                "queens",
                "queens",
                "brooklyn",
                "brooklyn",
                "manhattan",
                "manhattan",
                "bronx",
                "bronx",
            ],
            [
                "brooklyn",
                "manhattan",
                "brooklyn",
                "manhattan",
                "queens",
                "manhattan",
                "queens",
                "bronx",
                "queens",
                "manhattan",
            ],
            np.ones(10),
        )
        assert g == expected, (
            "sparse csr nybb with ids does not match arrays constructor"
        )
        np.testing.assert_array_equal(g.sparse.todense(), sp.todense())

        with pytest.raises(ValueError, match="The length of ids "):
            graph.Graph.from_sparse(sp, ids=["staten_island", "queens"])

    def test_from_arrays(self):
        focal_ids = np.arange(9)
        neighbor_ids = np.array([1, 2, 5, 4, 5, 8, 7, 8, 7])
        weight = np.array([1, 1, 1, 1, 1, 1, 1, 1, 1])

        g = graph.Graph.from_arrays(focal_ids, neighbor_ids, weight)
        pd.testing.assert_series_equal(
            g._adjacency,
            self.adjacency_int_binary,
            check_index_type=False,
            check_dtype=False,
        )

        focal_ids = np.asarray(list(self.neighbor_dict_str.keys()))
        neighbor_ids = np.asarray(list(self.neighbor_dict_str.values()))

        g = graph.Graph.from_arrays(focal_ids, neighbor_ids, weight)
        pd.testing.assert_series_equal(
            g._adjacency,
            self.adjacency_str_binary,
            check_index_type=False,
            check_dtype=False,
        )

    def test_from_weights_dict(self):
        weights_dict = {
            0: {2: 0.5, 1: 0.5},
            1: {0: 0.5, 3: 0.5},
            2: {
                0: 0.3,
                4: 0.3,
                3: 0.3,
            },
            3: {
                1: 0.3,
                2: 0.3,
                5: 0.3,
            },
            4: {2: 0.5, 5: 0.5},
            5: {3: 0.5, 4: 0.5},
        }
        exp_focal = pd.Index(
            [0, 0, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 5, 5], dtype="int64", name="focal"
        )
        exp_neighbor = [2, 1, 0, 3, 0, 4, 3, 1, 2, 5, 2, 5, 3, 4]
        exp_weight = [
            0.5,
            0.5,
            0.5,
            0.5,
            0.3,
            0.3,
            0.3,
            0.3,
            0.3,
            0.3,
            0.5,
            0.5,
            0.5,
            0.5,
        ]
        expected = graph.Graph.from_arrays(exp_focal, exp_neighbor, exp_weight)

        g = graph.Graph.from_weights_dict(weights_dict)
        assert g == expected

    def test_from_dicts(self):
        g = graph.Graph.from_dicts(self.neighbor_dict_int)
        pd.testing.assert_series_equal(
            g._adjacency,
            self.adjacency_int_binary,
            check_dtype=False,
            check_index_type=False,
        )

        g = graph.Graph.from_dicts(self.neighbor_dict_str)
        pd.testing.assert_series_equal(
            g._adjacency,
            self.adjacency_str_binary,
            check_dtype=True,
            check_index_type=False,
        )

    @pytest.mark.parametrize("y", [3, 5])
    @pytest.mark.parametrize("id_type", ["int", "str"])
    @pytest.mark.parametrize("rook", [True, False])
    def test_from_dicts_via_w(self, y, id_type, rook):
        w = weights.lat2W(3, y, id_type=id_type, rook=rook)
        g = graph.Graph.from_dicts(w.neighbors, w.weights)
        pd.testing.assert_series_equal(
            g._adjacency.sort_index(),
            w.to_adjlist().set_index(["focal", "neighbor"])["weight"].sort_index(),
            check_index_type=False,
            check_dtype=False,
        )

        w.transform = "r"
        g_rowwise = graph.Graph.from_dicts(w.neighbors, w.weights)
        pd.testing.assert_series_equal(
            g_rowwise._adjacency.sort_index(),
            w.to_adjlist().set_index(["focal", "neighbor"])["weight"].sort_index(),
            check_index_type=False,
            check_dtype=False,
        )

        diag = weights.fill_diagonal(w)
        g_diag = graph.Graph.from_dicts(diag.neighbors, diag.weights)
        pd.testing.assert_series_equal(
            g_diag._adjacency.sort_index(),
            diag.to_adjlist().set_index(["focal", "neighbor"])["weight"].sort_index(),
            check_index_type=False,
            check_dtype=False,
        )

        w = weights.W(
            neighbors={"a": ["b"], "b": ["a", "c"], "c": ["b"], "d": []},
            silence_warnings=True,
        )
        g_isolate = graph.Graph.from_dicts(w.neighbors, w.weights)
        pd.testing.assert_series_equal(
            g_isolate._adjacency.sort_index(),
            w.to_adjlist().set_index(["focal", "neighbor"])["weight"].sort_index(),
            check_index_type=False,
            check_dtype=False,
        )

    def test_neighbors(self):
        expected = {
            0: (0, 1, 3),
            1: (0, 2, 4),
            2: (1, 5),
            3: (0, 4, 6),
            4: (1, 3, 5, 7),
            5: (2, 4, 8),
            6: (3, 7),
            7: (4, 6, 8),
            8: (5, 7),
            9: (),
        }
        assert self.g_int.neighbors == expected

        expected = {
            "a": ("a", "b", "d"),
            "b": ("a", "c", "e"),
            "c": ("b", "f"),
            "d": ("a", "e", "g"),
            "e": ("b", "d", "f", "h"),
            "f": ("c", "e", "i"),
            "g": ("d", "h"),
            "h": ("e", "g", "i"),
            "i": ("f", "h"),
            "j": (),
        }
        assert self.g_str.neighbors == expected

    def test_weights(self):
        expected = {
            0: (1.0, 0.5, 0.5),
            1: (0.3, 0.3, 0.3),
            2: (0.5, 0.5),
            3: (0.3, 0.3, 0.3),
            4: (0.25, 0.25, 0.25, 0.25),
            5: (0.3, 0.3, 0.3),
            6: (0.5, 0.5),
            7: (0.3, 0.3, 0.3),
            8: (0.5, 0.5),
            9: (),
        }
        assert self.g_int.weights == expected

        expected = {
            "a": (1.0, 0.5, 0.5),
            "b": (0.3, 0.3, 0.3),
            "c": (0.5, 0.5),
            "d": (0.3, 0.3, 0.3),
            "e": (0.25, 0.25, 0.25, 0.25),
            "f": (0.3, 0.3, 0.3),
            "g": (0.5, 0.5),
            "h": (0.3, 0.3, 0.3),
            "i": (0.5, 0.5),
            "j": (),
        }
        assert self.g_str.weights == expected

    def test_sparse(self):
        sp = self.g_int.sparse
        expected = np.array(
            [
                [1.0, 0.5, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
                [0.3, 0.0, 0.3, 0.0, 0.3, 0.0, 0.0, 0.0, 0.0, 0.0],
                [0.0, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0],
                [0.3, 0.0, 0.0, 0.0, 0.3, 0.0, 0.3, 0.0, 0.0, 0.0],
                [0.0, 0.25, 0.0, 0.25, 0.0, 0.25, 0.0, 0.25, 0.0, 0.0],
                [0.0, 0.0, 0.3, 0.0, 0.3, 0.0, 0.0, 0.0, 0.3, 0.0],
                [0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0],
                [0.0, 0.0, 0.0, 0.0, 0.3, 0.0, 0.3, 0.0, 0.3, 0.0],
                [0.0, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.5, 0.0, 0.0],
                [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
            ]
        )
        np.testing.assert_array_equal(sp.todense(), expected)

        sp = self.g_str.sparse
        np.testing.assert_array_equal(sp.todense(), expected)

        sp_old = self.g_int.to_W().sparse.todense()
        np.testing.assert_array_equal(sp.todense(), sp_old)

        sp_old = self.g_str.to_W().sparse.todense()
        np.testing.assert_array_equal(sp.todense(), sp_old)

        # check proper sorting
        nybb = graph.Graph.build_contiguity(self.nybb)
        nybb_expected = np.array(
            [
                [0, 0, 0, 0, 0],
                [0, 0, 1, 1, 1],
                [0, 1, 0, 1, 0],
                [0, 1, 1, 0, 1],
                [0, 1, 0, 1, 0],
            ]
        )
        np.testing.assert_array_equal(nybb.sparse.todense(), nybb_expected)

    def test_sparse_roundtrip(self):
        g = graph.Graph(self.adjacency_int_binary)
        sp = g.sparse
        g_sp = graph.Graph.from_sparse(sp, self.index_int)
        assert g == g_sp

        g = graph.Graph(self.adjacency_str_binary)
        sp = g.sparse
        g_sp = graph.Graph.from_sparse(sp, self.index_str)
        assert g == g_sp

    def test_cardinalities(self):
        expected = pd.Series(
            [3, 3, 2, 3, 4, 3, 2, 3, 2, 0],
            index=pd.Index(
                ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"],
                name="focal",
            ),
            name="cardinalities",
        )
        pd.testing.assert_series_equal(self.g_str.cardinalities, expected)

    def test_isolates(self):
        expected = pd.Index(["j"], name="focal")
        pd.testing.assert_index_equal(self.g_str.isolates, expected)

        self.g_str._adjacency.iloc[1] = 0  # zero weight, no isolate
        pd.testing.assert_index_equal(self.g_str.isolates, expected)

        with_additional_zeros = self.g_str.assign_self_weight(0)
        pd.testing.assert_index_equal(with_additional_zeros.isolates, expected)

    def test_n(self):
        assert self.g_int.n == 10
        assert self.g_str.n == 10
        assert graph.Graph(self.adjacency_int_binary).n == 9

    def test_pct_nonzero(self):
        assert self.g_int.pct_nonzero == 26.0
        assert graph.Graph(self.adjacency_int_binary).pct_nonzero == pytest.approx(
            11.1111111111
        )

    def test_nonzero(self):
        assert self.g_int.nonzero == 25
        assert graph.Graph(self.adjacency_int_binary).nonzero == 9

    def test_index_pairs(self):
        focal, neighbor = self.g_str.index_pairs
        exp_focal = pd.Index(
            [
                "a",
                "a",
                "a",
                "b",
                "b",
                "b",
                "c",
                "c",
                "d",
                "d",
                "d",
                "e",
                "e",
                "e",
                "e",
                "f",
                "f",
                "f",
                "g",
                "g",
                "h",
                "h",
                "h",
                "i",
                "i",
                "j",
            ],
            name="focal",
        )
        exp_neighbor = pd.Index(
            [
                "a",
                "b",
                "d",
                "a",
                "c",
                "e",
                "b",
                "f",
                "a",
                "e",
                "g",
                "b",
                "d",
                "f",
                "h",
                "c",
                "e",
                "i",
                "d",
                "h",
                "e",
                "g",
                "i",
                "f",
                "h",
                "j",
            ],
            name="neighbor",
        )
        pd.testing.assert_index_equal(exp_focal, focal)
        pd.testing.assert_index_equal(exp_neighbor, neighbor)

    def test_transform_r(self):
        expected_w = [
            0.5,
            0.25,
            0.25,
            0.33333333,
            0.33333333,
            0.33333333,
            0.5,
            0.5,
            0.33333333,
            0.33333333,
            0.33333333,
            0.25,
            0.25,
            0.25,
            0.25,
            0.33333333,
            0.33333333,
            0.33333333,
            0.5,
            0.5,
            0.33333333,
            0.33333333,
            0.33333333,
            0.5,
            0.5,
            0.0,
        ]
        exp = self.g_int.adjacency
        exp.iloc[:] = expected_w
        expected = graph.Graph(exp)
        assert self.g_int.transform("r") == expected
        assert self.g_int.transform("r").transformation == "R"
        assert self.g_int.transform("R") == expected

        w = self.g_int.to_W()
        w.transform = "r"
        g_from_w = graph.Graph.from_W(w)
        assert g_from_w == self.g_int.transform("r")

    def test_transform_b(self):
        expected_w = [
            1,
            1,
            1,
            1,
            1,
            1,
            1,
            1,
            1,
            1,
            1,
            1,
            1,
            1,
            1,
            1,
            1,
            1,
            1,
            1,
            1,
            1,
            1,
            1,
            1,
            0,
        ]
        exp = self.g_int.adjacency
        exp.iloc[:] = expected_w
        expected = graph.Graph(exp)
        assert self.g_int.transform("b") == expected
        assert self.g_int.transform("b").transformation == "B"
        assert self.g_int.transform("B") == expected

        w = self.g_int.to_W()
        w.transform = "b"
        g_from_w = graph.Graph.from_W(w)
        assert g_from_w == self.g_int.transform("b")

    def test_transform_d(self):
        expected_w = [
            0.10416667,
            0.05208333,
            0.05208333,
            0.03125,
            0.03125,
            0.03125,
            0.05208333,
            0.05208333,
            0.03125,
            0.03125,
            0.03125,
            0.02604167,
            0.02604167,
            0.02604167,
            0.02604167,
            0.03125,
            0.03125,
            0.03125,
            0.05208333,
            0.05208333,
            0.03125,
            0.03125,
            0.03125,
            0.05208333,
            0.05208333,
            0.0,
        ]
        exp = self.g_int.adjacency
        exp.iloc[:] = expected_w
        expected = graph.Graph(exp)
        assert self.g_int.transform("d") == expected
        assert self.g_int.transform("d").transformation == "D"
        assert self.g_int.transform("D") == expected
        assert self.g_int.transform("D")._adjacency.sum() == pytest.approx(1)

        w = self.g_int.to_W()
        w.transform = "d"
        g_from_w = graph.Graph.from_W(w)
        assert g_from_w == self.g_int.transform("d")

    def test_transform_v(self):
        expected_w = [
            0.55154388,
            0.27577194,
            0.27577194,
            0.39000042,
            0.39000042,
            0.39000042,
            0.47765102,
            0.47765102,
            0.39000042,
            0.39000042,
            0.39000042,
            0.33775027,
            0.33775027,
            0.33775027,
            0.33775027,
            0.39000042,
            0.39000042,
            0.39000042,
            0.47765102,
            0.47765102,
            0.39000042,
            0.39000042,
            0.39000042,
            0.47765102,
            0.47765102,
            0.0,
        ]
        exp = self.g_int.adjacency
        exp.iloc[:] = expected_w
        expected = graph.Graph(exp)
        assert self.g_int.transform("v") == expected
        assert self.g_int.transform("v").transformation == "V"
        assert self.g_int.transform("V") == expected

        w = self.g_int.to_W()
        w.transform = "v"
        g_from_w = graph.Graph.from_W(w)
        assert g_from_w == self.g_int.transform("v")

    def test_transform(self):
        # do not transform if transformation == current transformation
        binary = self.g_int.transform("b")
        fast_tracked = binary.transform("b")
        assert binary == fast_tracked

        with pytest.raises(ValueError, match="Transformation 'X' is not"):
            self.g_int.transform("x")

    def test_transform_callable(self):
        contig = graph.Graph.build_contiguity(self.nybb)
        trans = contig.transform(lambda x: x * 10)
        assert trans.transformation == "C"
        assert trans.adjacency.sum() == 100

    def test_asymmetry(self):
        neighbors = {
            "a": ["b", "c", "d"],
            "b": ["b", "c", "d"],
            "c": ["a", "b"],
            "d": ["a", "b"],
        }
        weights_d = {"a": [1, 0.5, 1], "b": [1, 1, 1], "c": [1, 1], "d": [1, 1]}
        g = graph.Graph.from_dicts(neighbors, weights_d)
        intrinsic = pd.Series(
            ["b", "c", "a", "a"],
            index=pd.Index(["a", "a", "b", "c"], name="focal"),
            name="neighbor",
        )
        pd.testing.assert_series_equal(intrinsic, g.asymmetry())

        boolean = pd.Series(
            ["b", "a"],
            index=pd.Index(["a", "b"], name="focal"),
            name="neighbor",
        )
        pd.testing.assert_series_equal(boolean, g.asymmetry(intrinsic=False))

        empty = pd.Series(
            index=pd.Index([], name="focal"),
            name="neighbor",
            dtype="int64",
        )

        pd.testing.assert_series_equal(self.g_int.asymmetry(False), empty)

    def test_parquet(self):
        pytest.importorskip("pyarrow")

        with tempfile.TemporaryDirectory() as tmpdir:
            path = os.path.join(tmpdir, "g_int.parquet")
            self.g_int.to_parquet(path)
            g_int = graph.read_parquet(path)
            assert self.g_int == g_int

            path = os.path.join(tmpdir, "g_str.parquet")
            self.g_str.to_parquet(path)
            g_str = graph.read_parquet(path)
            assert self.g_str == g_str

            row_wise = self.g_str.transform("r")
            path = os.path.join(tmpdir, "row.parquet")
            row_wise.to_parquet(path)
            row_read = graph.read_parquet(path)
            assert row_wise == row_read
            assert row_read.transformation == "R"

            path = os.path.join(tmpdir, "pandas.parquet")
            self.g_str._adjacency.to_frame().to_parquet(path)
            g_pandas = graph.read_parquet(path)
            assert self.g_str == g_pandas

    def test_gal(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            path = os.path.join(tmpdir, "g_int.gal")
            g_int = self.g_int.transform("b")
            g_int.to_gal(path)
            g_int_ = graph.read_gal(path)
            assert g_int == g_int_

            path = os.path.join(tmpdir, "g_str.gal")
            g_str = self.g_str.transform("b")
            g_str.to_gal(path)
            g_str_ = graph.read_gal(path)
            assert g_str == g_str_

    def test_gwt(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            path = os.path.join(tmpdir, "g_int.gwt")
            self.g_int.to_gwt(path)
            g_int = graph.read_gwt(path)
            assert self.g_int == g_int

            path = os.path.join(tmpdir, "g_str.gwt")
            self.g_str.to_gwt(path)
            g_str = graph.read_gwt(path)
            assert self.g_str == g_str

    def test_getitem(self):
        expected = pd.Series(
            [1, 0.5, 0.5],
            index=pd.Index(["a", "b", "d"], name="neighbor"),
            name="weight",
        )
        pd.testing.assert_series_equal(expected, self.g_str["a"])

        expected = pd.Series(
            [1, 0.5, 0.5],
            index=pd.Index([0, 1, 3], name="neighbor"),
            name="weight",
        )
        pd.testing.assert_series_equal(expected, self.g_int[0])

        # isolate
        expected = pd.Series(
            [],
            index=pd.Index([], name="neighbor"),
            name="weight",
        )
        pd.testing.assert_series_equal(expected, self.g_str["j"])

    def test_lag(self):
        expected = np.array([4.0, 2.7, 4.0, 3.9, 5.0, 5.1, 6.0, 6.3, 7.0, 0.0])
        lag = self.g_str.lag(list(range(1, 11)))

        np.testing.assert_allclose(expected, lag)

        with pytest.raises(ValueError, match="The length of `y`"):
            self.g_str.lag(list(range(1, 15)))

    def test_higher_order(self):
        cont = graph.Graph.build_contiguity(self.nybb)
        k2 = cont.higher_order(2)
        expected = graph.Graph.from_arrays(
            self.nybb.index,
            ["Staten Island", "Queens", "Bronx", "Manhattan", "Brooklyn"],
            [0, 0, 1, 0, 1],
        )
        assert k2 == expected

        diagonal = cont.higher_order(2, diagonal=True)
        expected = graph.Graph.from_arrays(
            [
                "Staten Island",
                "Queens",
                "Brooklyn",
                "Brooklyn",
                "Manhattan",
                "Bronx",
                "Bronx",
            ],
            [
                "Staten Island",
                "Queens",
                "Brooklyn",
                "Bronx",
                "Manhattan",
                "Brooklyn",
                "Bronx",
            ],
            [0, 1, 1, 1, 1, 1, 1],
        )
        assert diagonal == expected

        shortest_false = cont.higher_order(2, shortest_path=False)
        expected = graph.Graph.from_arrays(
            [
                "Staten Island",
                "Queens",
                "Queens",
                "Queens",
                "Brooklyn",
                "Brooklyn",
                "Brooklyn",
                "Manhattan",
                "Manhattan",
                "Manhattan",
                "Bronx",
                "Bronx",
                "Bronx",
            ],
            [
                "Staten Island",
                "Brooklyn",
                "Manhattan",
                "Bronx",
                "Queens",
                "Manhattan",
                "Bronx",
                "Queens",
                "Brooklyn",
                "Bronx",
                "Queens",
                "Brooklyn",
                "Manhattan",
            ],
            [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        )
        assert shortest_false == expected

        lower = cont.higher_order(2, lower_order=True)
        assert lower == expected

    def test_higher_order_inclusive(self):  # GH738
        contig = graph.Graph.from_arrays(
            [0, 1, 2, 3, 3, 4, 4], [0, 3, 4, 1, 4, 2, 3], [0, 1, 1, 1, 1, 1, 1]
        )
        assert len(contig) == 6
        higher = contig.higher_order(2, lower_order=True)
        assert len(higher) == 10
        assert contig < higher

    def test_n_components(self):
        nybb = graph.Graph.build_contiguity(self.nybb)
        assert nybb.n_components == 2

        nybb = graph.Graph.build_knn(self.nybb.set_geometry(self.nybb.centroid), k=2)
        assert nybb.n_components == 1

    def test_component_labels(self):
        nybb = graph.Graph.build_contiguity(self.nybb)
        expected = pd.Series(
            [0, 1, 1, 1, 1],
            index=pd.Index(self.nybb.index.values, name="focal"),
            dtype=int,
            name="component labels",
        )
        pd.testing.assert_series_equal(
            expected, nybb.component_labels, check_dtype=False
        )

    def test_eliminate_zeros(self):
        nybb = graph.Graph.build_contiguity(self.nybb)
        adj = nybb._adjacency.copy()
        adj["Bronx", "Queens"] = 0
        adj["Queens", "Manhattan"] = 0
        adj["Queens", "Queens"] = 0
        with_zero = graph.Graph(adj)
        expected = adj.drop(
            [("Bronx", "Queens"), ("Queens", "Manhattan"), ("Queens", "Queens")]
        )
        pd.testing.assert_series_equal(with_zero.eliminate_zeros()._adjacency, expected)

    def test_subgraph(self):
        knn = graph.Graph.build_knn(self.nybb.set_geometry(self.nybb.centroid), k=2)
        sub = knn.subgraph(["Staten Island", "Bronx", "Brooklyn"])
        assert sub < knn
        expected = pd.Series(
            [1, 0, 0],
            name="weight",
            index=pd.MultiIndex.from_arrays(
                [
                    ["Staten Island", "Bronx", "Brooklyn"],
                    ["Brooklyn", "Bronx", "Brooklyn"],
                ],
                names=["focal", "neighbor"],
            ),
        )
        pd.testing.assert_series_equal(expected, sub._adjacency, check_dtype=False)

    def test_assign_self_weight(self):
        contig = graph.Graph.build_contiguity(self.nybb)
        diag = contig.assign_self_weight()
        assert len(diag._adjacency) == 15
        assert diag._adjacency.sum() == 15

        diag_array = contig.assign_self_weight([2, 3, 4, 5, 6])
        assert len(diag_array._adjacency) == 15
        assert diag_array._adjacency.sum() == 30

        for i, val in enumerate(range(2, 7)):
            assert (
                diag_array._adjacency[(contig.unique_ids[i], contig.unique_ids[i])]
                == val
            )

    def test_apply(self):
        contig = graph.Graph.build_contiguity(self.nybb)

        # pandas built-in
        expected = pd.Series(
            [1.62382200e09, 3.76087588e09, 3.68168493e09, 6.16961834e09, 3.68168493e09],
            index=pd.Index(
                ["Staten Island", "Queens", "Brooklyn", "Manhattan", "Bronx"],
                name="focal",
            ),
        )
        pd.testing.assert_series_equal(contig.apply(self.nybb.area, "sum"), expected)

        # numpy
        expected = pd.Series(
            [1.62382200e09, 1.18692629e09, 1.84084247e09, 1.93747835e09, 1.84084247e09],
            index=pd.Index(
                ["Staten Island", "Queens", "Brooklyn", "Manhattan", "Bronx"],
                name="focal",
            ),
        )
        pd.testing.assert_series_equal(
            contig.apply(self.nybb.area, np.median), expected
        )

        # lambda over geometry
        expected = pd.Series(
            [2.06271959e09, 6.68788190e09, 7.57087991e09, 8.78957337e09, 7.57087991e09],
            index=pd.Index(
                ["Staten Island", "Queens", "Brooklyn", "Manhattan", "Bronx"],
                name="focal",
            ),
        )
        pd.testing.assert_series_equal(
            contig.apply(self.nybb.geometry, lambda x: x.unary_union.convex_hull.area),
            expected,
        )

        # reduction over a dataframe
        expected = pd.DataFrame(
            [
                [3.30470010e05, 1.62381982e09],
                [1.56477261e06, 3.76087473e09],
                [1.25564314e06, 3.68168433e09],
                [2.10181756e06, 6.16961599e09],
                [1.25564314e06, 3.68168433e09],
            ],
            columns=["Shape_Leng", "Shape_Area"],
            index=pd.Index(
                ["Staten Island", "Queens", "Brooklyn", "Manhattan", "Bronx"],
                name="focal",
            ),
        )
        pd.testing.assert_frame_equal(
            contig.apply(
                self.nybb, lambda x: x[["Shape_Leng", "Shape_Area"]].sum(axis=0)
            ),
            expected,
        )

        # 1D array input
        expected = pd.Series(
            [1.62382200e09, 3.76087588e09, 3.68168493e09, 6.16961834e09, 3.68168493e09],
            index=pd.Index(
                ["Staten Island", "Queens", "Brooklyn", "Manhattan", "Bronx"],
                name="focal",
            ),
        )
        pd.testing.assert_series_equal(
            contig.apply(self.nybb.area.values, "sum"), expected
        )

        # 2D array input
        expected = pd.DataFrame(
            [
                [3.30470010e05, 1.62381982e09],
                [1.56477261e06, 3.76087473e09],
                [1.25564314e06, 3.68168433e09],
                [2.10181756e06, 6.16961599e09],
                [1.25564314e06, 3.68168433e09],
            ],
            index=pd.Index(
                ["Staten Island", "Queens", "Brooklyn", "Manhattan", "Bronx"],
                name="focal",
            ),
        )
        pd.testing.assert_frame_equal(
            contig.apply(
                self.nybb[["Shape_Leng", "Shape_Area"]].values,
                lambda x: x.sum(axis=0),
            ),
            expected,
        )

    def test_aggregate(self):
        contig = graph.Graph.build_contiguity(self.nybb)
        expected = pd.Series(
            [1.0, 20.08553692, 7.3890561, 20.08553692, 7.3890561],
            index=pd.Index(
                ["Staten Island", "Queens", "Brooklyn", "Manhattan", "Bronx"],
                name="focal",
            ),
            name="weight",
        )
        pd.testing.assert_series_equal(
            contig.aggregate(lambda x: np.exp(np.sum(x))),
            expected,
        )

    def test_describe(self):
        contig = graph.Graph.build_knn(self.guerry.geometry.centroid, k=5)
        y = self.guerry.geometry.area
        stats = contig.describe(y)
        pd.testing.assert_series_equal(
            stats["count"],
            contig.cardinalities,
            check_names=False,
            check_dtype=False,
        )
        pd.testing.assert_series_equal(
            stats["sum"],
            pd.Series(contig.lag(y), index=contig.unique_ids),
            check_names=False,
        )
        r_contig = contig.transform("R")
        pd.testing.assert_series_equal(
            stats["mean"],
            pd.Series(r_contig.lag(y), index=contig.unique_ids),
            check_names=False,
        )
        ## compute only some statistics
        specific_stats = contig.describe(y, statistics=["count", "sum", "mean"])
        ## assert only the specified values are computed
        assert list(specific_stats.columns) == ["count", "sum", "mean"]

        pd.testing.assert_frame_equal(
            specific_stats[["count", "sum", "mean"]], stats[["count", "sum", "mean"]]
        )

        percentile_stats = contig.describe(y, q=(25, 75))

        for i in contig.unique_ids:
            neigh_vals = y[contig[i].index.values]
            low, high = neigh_vals.describe()[["25%", "75%"]]
            neigh_vals = neigh_vals[(low <= neigh_vals) & (neigh_vals <= high)]
            expected = neigh_vals.describe()[["count", "mean", "std", "min", "max"]]
            res = percentile_stats.loc[i][["count", "mean", "std", "min", "max"]]
            pd.testing.assert_series_equal(res, expected, check_names=False)

        ## test NA equivalence between filtration and pandas
        nan_areas = y.copy()
        nan_areas.iloc[range(0, len(y), 3),] = np.nan
        res1 = contig.describe(y, statistics=["count"])["count"]
        res2 = contig.describe(y, statistics=["count"], q=(0, 100))["count"]
        pd.testing.assert_series_equal(res1, res2)

        # test with isolates and string index
        nybb_contig = graph.Graph.build_contiguity(self.nybb, rook=False)
        stats = nybb_contig.describe(
            self.nybb.geometry.area, statistics=["count", "sum", "mode"]
        )
        ## all isolate values should be nan
        assert stats.loc["Staten Island"].isna().all()

        # for easier comparison and na has already been checked.
        stats = stats.fillna(0)

        pd.testing.assert_series_equal(
            stats["sum"],
            pd.Series(nybb_contig.lag(self.nybb.geometry.area), index=self.nybb.index),
            check_names=False,
        )

        pd.testing.assert_series_equal(
            stats["count"].sort_index(),
            nybb_contig.cardinalities.sort_index(),
            check_dtype=False,
            check_names=False,
        )

        y = self.nybb.geometry.area
        for i in nybb_contig.unique_ids:
            neigh_vals = y.loc[nybb_contig[i].index.values]
            expected = neigh_vals.mode().iloc[0] if neigh_vals.shape[0] else 0
            res = stats.loc[i]["mode"]
            assert res == expected

        ## test passing ndarray
        stats1 = nybb_contig.describe(self.nybb.geometry.area, statistics=["sum"])[
            "sum"
        ]
        stats2 = nybb_contig.describe(
            self.nybb.geometry.area.values, statistics=["sum"]
        )["sum"]
        pd.testing.assert_series_equal(
            stats1,
            stats2,
            check_dtype=False,
            check_names=False,
        )

        ## test index alignment
        with pytest.raises(
            ValueError, match="The values index is not aligned with the graph index."
        ):
            nybb_contig.describe(self.nybb.geometry.area.reset_index(drop=True))

    def test_summary(self):
        assert isinstance(self.g_int.summary(), graph.GraphSummary)
        assert isinstance(self.g_str.summary(), graph.GraphSummary)
        assert isinstance(self.g_str_unodered.summary(), graph.GraphSummary)
