"""
tests/test_plot_contracts
~~~~~~~~~~~~~~~~~~~~~~~~~
"""

import matplotlib.pyplot as plt
import pandas as pd
import pytest
from matplotlib.colors import Normalize, to_rgba

from conftest import extract_figure_text, use_agg_backend
from himalayas import Results
from himalayas.core.clustering import cluster
from himalayas.plot import Plotter
from himalayas.plot.renderers._label_format import format_label_prefix
from himalayas.plot.renderers.cluster_labels import _build_label_map, _parse_label_overrides
from himalayas.plot.track_layout import TrackLayoutManager


@pytest.fixture(autouse=True)
def _close_all_figures_after_each_test():
    """
    Ensures pyplot figures do not accumulate across tests in this module.
    """
    yield
    plt.close("all")


@pytest.mark.api
def test_plotter_smoke(toy_results):
    """
    Ensures Plotter can render a minimal plot without errors.

    Args:
        toy_results (Results): Results fixture with clusters and layout.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        (
            Plotter(toy_results)
            .set_label_panel(
                axes=[0.70, 0.05, 0.29, 0.90],
                text_pad=0.01,
            )
            .plot_matrix()
            .plot_cluster_labels()
            .show()
        )
    finally:
        plt.show = plt_show


@pytest.mark.api
def test_plotter_stacked_defaults_smoke(toy_results):
    """
    Ensures a stacked default Plotter chain renders without explicit style kwargs
    and forwards row/column tick padding.

    Args:
        toy_results (Results): Results fixture with clusters and layout.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        plotter = (
            Plotter(toy_results)
            .set_label_panel(
                axes=[0.70, 0.05, 0.29, 0.90],
                track_x=0.02,
                gutter_width=0.01,
                text_pad=0.01,
            )
            .plot_dendrogram()
            .plot_matrix()
            .plot_matrix_axis_labels()
            .plot_row_ticks(pad=7)
            .plot_col_ticks(pad=11)
            .plot_cluster_labels()
            .plot_cluster_bar(name="sigbar")
        )
        plotter.show()
        row_tick_axes = [ax for ax in plotter._fig.axes if len(ax.get_yticklabels()) > 0]
        col_tick_axes = [ax for ax in plotter._fig.axes if len(ax.get_xticklabels()) > 0]
        assert row_tick_axes
        assert col_tick_axes
        assert row_tick_axes[0].yaxis.majorTicks[0].get_pad() == pytest.approx(7.0)
        assert col_tick_axes[0].xaxis.majorTicks[0].get_pad() == pytest.approx(11.0)
    finally:
        plt.show = plt_show


@pytest.mark.api
@pytest.mark.parametrize(
    "stack",
    [
        # Keep this matrix to valid combinations only: legend requires row_bar,
        # and cluster_bar requires cluster_labels.
        {
            "cluster_labels": False,
            "cluster_bar": False,
            "row_bar": False,
            "legend": False,
            "colorbar": False,
        },
        {
            "cluster_labels": True,
            "cluster_bar": False,
            "row_bar": False,
            "legend": False,
            "colorbar": False,
        },
        {
            "cluster_labels": True,
            "cluster_bar": True,
            "row_bar": False,
            "legend": False,
            "colorbar": False,
        },
        {
            "cluster_labels": False,
            "cluster_bar": False,
            "row_bar": True,
            "legend": False,
            "colorbar": False,
        },
        {
            "cluster_labels": False,
            "cluster_bar": False,
            "row_bar": True,
            "legend": True,
            "colorbar": False,
        },
        {
            "cluster_labels": False,
            "cluster_bar": False,
            "row_bar": False,
            "legend": False,
            "colorbar": True,
        },
        {
            "cluster_labels": False,
            "cluster_bar": False,
            "row_bar": True,
            "legend": True,
            "colorbar": True,
        },
        {
            "cluster_labels": True,
            "cluster_bar": True,
            "row_bar": True,
            "legend": True,
            "colorbar": True,
        },
    ],
    ids=[
        "matrix_only",
        "matrix_plus_cluster_labels",
        "matrix_plus_cluster_labels_and_bar",
        "matrix_plus_row_bar",
        "matrix_plus_row_bar_and_legend",
        "matrix_plus_colorbar",
        "matrix_plus_row_bar_legend_and_colorbar",
        "full_stack",
    ],
)
def test_plotter_stack_combinations_smoke(toy_results, stack):
    """
    Ensures representative plot-layer stack combinations render without fatal interactions.

    Args:
        toy_results (Results): Results fixture with clusters and layout.
        stack (dict): Boolean feature switches for enabled layers.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        plotter = Plotter(toy_results).plot_matrix()
        # Each layer is tested in isolation in other tests, so we do not assert on the
        # presence of rendered elements here. We just want to ensure no fatal errors occur
        # when stacking layers in various combinations.
        if stack["cluster_labels"]:
            plotter = plotter.plot_cluster_labels(label_fields=("label",))
        if stack["cluster_bar"]:
            plotter = plotter.plot_cluster_bar(name="sig")
        if stack["row_bar"]:
            plotter = plotter.plot_label_bar(
                values={"a": "1", "b": "2", "c": "1", "d": "2"},
                name="orf_category",
                mode="categorical",
                colors={"1": "#1b9e77", "2": "#d95f02"},
                title="ORF Category",
            )
        if stack["legend"]:
            plotter = plotter.add_label_legend(name="orf_category").plot_label_legends(
                height=0.07, gap=0.02, swatch_scale=0.9
            )
        if stack["colorbar"]:
            plotter = plotter.add_colorbar(
                name="score",
                cmap="viridis",
                norm=Normalize(vmin=0.0, vmax=1.0),
                ticks=[0.0, 1.0],
                label="Score",
            ).plot_colorbars()

        plotter.show()
        assert plotter._fig is not None
        if stack["legend"]:
            assert plotter.label_legend_layout_ is not None
        else:
            assert plotter.label_legend_layout_ is None
        if stack["colorbar"]:
            assert plotter.colorbar_layout_ is not None
        else:
            assert plotter.colorbar_layout_ is None
    finally:
        plt.show = plt_show


@pytest.mark.api
def test_plotter_requires_layers(toy_results):
    """
    Ensures Plotter refuses to render with no declared layers.

    Args:
        toy_results (Results): Results fixture with clusters and layout.

    Raises:
        RuntimeError: If no plot layers are declared.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        with pytest.raises(RuntimeError):
            Plotter(toy_results).show()
    finally:
        plt.show = plt_show


@pytest.mark.api
def test_plotter_requires_layout(toy_matrix):
    """
    Ensures Plotter errors when Results has no attached layout.

    Args:
        toy_matrix (Matrix): Toy matrix fixture.

    Raises:
        ValueError: If Results has no attached layout.
    """
    clusters = cluster(toy_matrix, linkage_threshold=1.0)
    results = Results(pd.DataFrame(), matrix=toy_matrix, clusters=clusters)

    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        with pytest.raises(ValueError):
            Plotter(results).plot_matrix().show()
    finally:
        plt.show = plt_show


@pytest.mark.api
def test_plot_cluster_bar_requires_cluster_labels_layer(toy_results):
    """
    Ensures cluster bars require the cluster label layer in the same plot chain.

    Args:
        toy_results (Results): Results fixture with clusters and layout.

    Raises:
        ValueError: If plot_cluster_bar is used without plot_cluster_labels.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        with pytest.raises(ValueError, match="plot_cluster_labels"):
            Plotter(toy_results).plot_matrix().plot_cluster_bar(name="sig").show()
    finally:
        plt.show = plt_show


@pytest.mark.api
def test_plot_cluster_bar_uses_internal_cluster_labels(toy_results):
    """
    Ensures plot_cluster_bar renders from internally generated cluster labels.

    Args:
        toy_results (Results): Results fixture with clusters and layout.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        (
            Plotter(toy_results)
            .plot_matrix()
            .plot_cluster_labels()
            .plot_cluster_bar(name="sig")
            .show()
        )
    finally:
        plt.show = plt_show


@pytest.mark.api
def test_plotter_plot_handle_rebuilds_closed_figure(toy_results, tmp_path):
    """
    Ensures Plotter rebuilds after the backing figure is closed.

    Args:
        toy_results (Results): Results fixture with clusters and layout.
        tmp_path (Path): Temporary output directory.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    out = tmp_path / "plotter_rebuild.png"
    plotter = None
    plt.show = lambda *args, **kwargs: None
    try:
        plotter = Plotter(toy_results).plot_matrix()

        # Baseline render: capture the original figure handle.
        plotter.show()
        fig0 = plotter._fig
        # Simulate notebook/backend lifecycle where figures are explicitly closed.
        plt.close(fig0)

        # Closed handle should trigger a rebuild on show().
        plotter.show()
        assert plotter._fig is not None
        assert plotter._fig is not fig0
        assert plotter._fig.number in plt.get_fignums()

        # save() should also rebuild after close and produce an output file.
        plt.close(plotter._fig)
        plotter.save(out, dpi=150)
        assert out.exists()
        assert out.stat().st_size > 0
        assert plotter._fig.number in plt.get_fignums()
    finally:
        plt.show = plt_show
        if (
            plotter is not None
            and plotter._fig is not None
            and plotter._fig.number in plt.get_fignums()
        ):
            plt.close(plotter._fig)


@pytest.mark.api
def test_plot_colorbars_tick_decimals_contract(toy_results):
    """
    Ensures colorbar precision and label spacing contracts are consistent.

    Args:
        toy_results (Results): Results fixture with clusters and layout.

    Raises:
        TypeError: If tick_decimals is not an integer or label_pad is invalid.
        ValueError: If tick_decimals is negative or label_pad is negative.
    """
    with pytest.raises(TypeError, match="tick_decimals"):
        Plotter(toy_results).plot_colorbars(tick_decimals=1.5)
    with pytest.raises(TypeError, match="tick_decimals"):
        Plotter(toy_results).plot_colorbars(tick_decimals=True)
    with pytest.raises(ValueError, match="tick_decimals"):
        Plotter(toy_results).plot_colorbars(tick_decimals=-1)
    with pytest.raises(TypeError, match="label_pad"):
        Plotter(toy_results).plot_colorbars(label_pad="2")
    with pytest.raises(TypeError, match="label_pad"):
        Plotter(toy_results).plot_colorbars(label_pad=True)
    with pytest.raises(ValueError, match="label_pad"):
        Plotter(toy_results).plot_colorbars(label_pad=-0.1)

    # Also test that valid arguments are accepted and reflected in the rendered colorbar.
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        plotter = (
            Plotter(toy_results)
            .plot_matrix()
            .add_colorbar(
                name="precision",
                cmap="viridis",
                norm=Normalize(vmin=0.0, vmax=1.0),
                ticks=[0.0, 0.1234, 1.0],
                label="Precision",
            )
            .plot_colorbars(tick_decimals=2, label_pad=7.0)
        )
        plotter.show()
        fig = plotter._fig
        tick_texts = [t.get_text() for ax in fig.axes for t in ax.get_xticklabels()]
        tick_texts = [txt for txt in tick_texts if txt]
        assert "0.12" in tick_texts
        assert "0.1234" not in tick_texts
        assert all("." not in txt or len(txt.split(".", 1)[1]) <= 2 for txt in tick_texts)
        precision_axes = [ax for ax in fig.axes if ax.get_xlabel() == "Precision"]
        assert len(precision_axes) == 1
        assert precision_axes[0].xaxis.labelpad == pytest.approx(7.0)
    finally:
        plt.show = plt_show


@pytest.mark.api
def test_plot_label_bar_requires_colors(toy_results):
    """
    Ensures categorical label bars require an explicit colors mapping.

    Args:
        toy_results (Results): Results fixture with clusters and layout.

    Raises:
        TypeError: If categorical mode is missing a colors mapping.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        with pytest.raises(TypeError):
            (
                Plotter(toy_results)
                .plot_matrix()
                .plot_label_bar(values={"a": "hit"}, mode="categorical")
                .show()
            )
    finally:
        plt.show = plt_show


@pytest.mark.api
@pytest.mark.parametrize(
    "kwargs, add_once_first, expected_match",
    [
        ({"name": ""}, False, "non-empty string"),
        ({"name": "orf_category"}, True, "already declared"),
        ({"name": "orf_category", "nrows": 0}, False, "nrows"),
        ({"name": "orf_category", "ncols": -1}, False, "ncols"),
        ({"name": "orf_category", "row_pad": -0.01}, False, "row_pad"),
        ({"name": "orf_category", "col_pad": -0.01}, False, "col_pad"),
    ],
    ids=[
        "empty_name",
        "duplicate_name",
        "nrows_nonpositive",
        "ncols_nonpositive",
        "row_pad_negative",
        "col_pad_negative",
    ],
)
def test_add_label_legend_rejects_invalid_inputs(
    toy_results,
    kwargs,
    add_once_first,
    expected_match,
):
    """
    Ensures add_label_legend validates name uniqueness and geometry arguments.

    Args:
        toy_results (Results): Results fixture with clusters and layout.
        kwargs (dict): Keyword arguments for add_label_legend.
        add_once_first (bool): Whether to add a valid legend before asserting duplicate rejection.
        expected_match (str): Regex fragment expected in the raised error message.

    Raises:
        ValueError: If legend declaration arguments are invalid.
    """
    plotter = (
        Plotter(toy_results)
        .plot_matrix()
        .plot_label_bar(
            values={"a": "1", "b": "2", "c": "1", "d": "2"},
            name="orf_category",
            mode="categorical",
            colors={"1": "#1b9e77", "2": "#d95f02"},
        )
    )
    if add_once_first:
        plotter.add_label_legend(name="orf_category")

    with pytest.raises(ValueError, match=expected_match):
        plotter.add_label_legend(**kwargs)


@pytest.mark.api
@pytest.mark.parametrize(
    "kwargs, expected_match",
    [
        ({"height": 0.0}, "height"),
        ({"gap": -0.01}, "gap"),
        ({"vpad": -0.01}, "vpad"),
        ({"title_pad": -1.0}, "title_pad"),
        ({"swatch_scale": 0.0}, "swatch_scale"),
    ],
    ids=[
        "height_nonpositive",
        "gap_negative",
        "vpad_negative",
        "title_pad_negative",
        "swatch_scale_nonpositive",
    ],
)
def test_plot_label_legends_rejects_invalid_geometry(
    toy_results,
    kwargs,
    expected_match,
):
    """
    Ensures plot_label_legends validates strip geometry arguments.

    Args:
        toy_results (Results): Results fixture with clusters and layout.
        kwargs (dict): Keyword arguments for plot_label_legends.
        expected_match (str): Regex fragment expected in the raised error message.

    Raises:
        ValueError: If legend strip geometry arguments are invalid.
    """
    with pytest.raises(ValueError, match=expected_match):
        Plotter(toy_results).plot_label_legends(**kwargs)


@pytest.mark.api
def test_plot_label_legends_require_source_colors_mapping(toy_results):
    """
    Ensures label legends fail when the referenced row bar payload lacks colors mapping.

    Args:
        toy_results (Results): Results fixture with clusters and layout.

    Raises:
        ValueError: If the referenced categorical row label bar has no colors mapping.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        plotter = (
            Plotter(toy_results)
            .plot_matrix()
            .plot_label_bar(
                values={"a": "1", "b": "2", "c": "1", "d": "2"},
                name="orf_category",
                mode="categorical",
                colors={"1": "#1b9e77", "2": "#d95f02"},
            )
            .add_label_legend(name="orf_category")
            .plot_label_legends()
        )
        # Corrupt track payload intentionally to verify legend source integrity checks.
        # Replace the row-track renderer to avoid earlier label-bar color validation.
        plotter._track_layout.tracks[0]["renderer"] = lambda *args, **kwargs: None
        plotter._track_layout.tracks[0]["payload"]["colors"] = None
        with pytest.raises(ValueError, match="non-empty `colors` mapping"):
            plotter.show()
    finally:
        plt.show = plt_show


@pytest.mark.api
def test_plot_label_legends_grid_overflow_raises(toy_results):
    """
    Ensures legend rendering errors when configured grid cannot fit all categories.

    Args:
        toy_results (Results): Results fixture with clusters and layout.

    Raises:
        ValueError: If nrows * ncols is smaller than the number of legend entries.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        with pytest.raises(ValueError, match="cannot fit all categories"):
            (
                Plotter(toy_results)
                .plot_matrix()
                .plot_label_bar(
                    values={"a": "1", "b": "2", "c": "3", "d": "1"},
                    name="orf_category",
                    mode="categorical",
                    colors={"1": "#1b9e77", "2": "#d95f02", "3": "#7570b3"},
                )
                .add_label_legend(name="orf_category", nrows=1, ncols=2)
                .plot_label_legends()
                .show()
            )
    finally:
        plt.show = plt_show


@pytest.mark.api
def test_plot_label_legends_render_categorical_track(toy_results):
    """
    Ensures categorical label legends render from a declared categorical row label bar.

    Args:
        toy_results (Results): Results fixture with clusters and layout.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        values = {"a": "1", "b": "2", "c": "1", "d": "2"}
        colors = {
            "1": "#1b9e77",
            "2": "#d95f02",
            "3": "#7570b3",  # Unused in matrix rows
        }
        plotter = (
            Plotter(toy_results)
            .plot_matrix()
            .plot_label_bar(
                values=values,
                name="orf_category",
                mode="categorical",
                colors=colors,
                title="ORF Category",
            )
            .add_label_legend(
                name="orf_category",
                nrows=1,
                row_pad=0.01,
                col_pad=0.015,
            )
            .plot_label_legends(height=0.07, gap=0.02, swatch_scale=0.9)
        )
        plotter.show()
        texts = extract_figure_text(plotter._fig, strip=True, nonempty=True)
        assert "ORF Category" in texts
        assert "1" in texts
        assert "2" in texts
        assert "3" not in texts
        assert plotter.label_legend_layout_ is not None
        assert plotter._label_legends[0]["row_pad"] == pytest.approx(0.01)
        assert plotter._label_legends[0]["col_pad"] == pytest.approx(0.015)
        assert plotter.label_legend_layout_["swatch_scale"] == pytest.approx(0.9)
    finally:
        plt.show = plt_show


@pytest.mark.api
def test_plot_label_legends_require_categorical_track_mode(toy_results):
    """
    Ensures categorical legends reject non-categorical row label bars.

    Args:
        toy_results (Results): Results fixture with clusters and layout.

    Raises:
        ValueError: If the referenced label bar is not categorical.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        with pytest.raises(ValueError, match="mode='categorical'"):
            (
                Plotter(toy_results)
                .plot_matrix()
                .plot_label_bar(
                    values={"a": 0.0, "b": 1.0, "c": 0.5, "d": 1.0},
                    name="continuous_track",
                    mode="continuous",
                    cmap="viridis",
                )
                .add_label_legend(name="continuous_track")
                .plot_label_legends()
                .show()
            )
    finally:
        plt.show = plt_show


@pytest.mark.api
def test_plot_label_legends_require_known_row_track(toy_results):
    """
    Ensures categorical legends reject unknown row label-bar references.

    Args:
        toy_results (Results): Results fixture with clusters and layout.

    Raises:
        ValueError: If the referenced label-bar name is unknown.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        with pytest.raises(ValueError, match="Unknown label-legend name"):
            (
                Plotter(toy_results)
                .plot_matrix()
                .add_label_legend(name="missing_track")
                .plot_label_legends()
                .show()
            )
    finally:
        plt.show = plt_show


@pytest.mark.api
def test_plot_cluster_labels_respect_np_order(toy_results):
    """
    Ensures cluster labels keep label first while respecting q/fe/p/n order from label_fields
    and emitting a single stats block.

    Args:
        toy_results (Results): Results fixture with clusters and layout.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        results_q = toy_results.with_qvalues()
        plotter = (
            Plotter(results_q)
            .plot_matrix()
            .plot_cluster_labels(
                label_fields=("label", "q", "fe", "p", "n"),
            )
        )
        plotter.show()
        fig = plotter._fig
        texts = extract_figure_text(fig)
        # Cluster labels are asserted via rendered text fragments rather than data objects.
        label_texts = [t for t in texts if "$q$=" in t and "FE=" in t and "$p$=" in t and "n=" in t]
        assert label_texts, "Expected cluster label text to be rendered."
        for txt in label_texts:
            assert " (" in txt
            assert txt.strip().endswith(")")
            assert "$q$=" in txt
            assert "FE=" in txt
            assert "$p$=" in txt
            assert "n=" in txt
            assert txt.find("$q$=") < txt.find("FE=")
            assert txt.find("FE=") < txt.find("$p$=")
            assert txt.find("$p$=") < txt.find("n=")
    finally:
        plt.show = plt_show


@pytest.mark.api
def test_plot_cluster_labels_invalid_label_prefix_raises(toy_results):
    """
    Ensures unsupported label_prefix values raise a ValueError.

    Args:
        toy_results (Results): Results fixture with clusters and layout.

    Raises:
        ValueError: If label_prefix is not supported.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        with pytest.raises(ValueError, match="label_prefix"):
            (
                Plotter(toy_results)
                .plot_matrix()
                .plot_cluster_labels(
                    label_fields=("label",),
                    label_prefix="bad",
                )
                .show()
            )
    finally:
        plt.show = plt_show


@pytest.mark.api
def test_plot_cluster_labels_none_label_fields_hides_text(toy_results):
    """
    Ensures label_fields=None suppresses cluster text when no prefix is requested.

    Args:
        toy_results (Results): Results fixture with clusters and layout.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        plotter = (
            Plotter(toy_results)
            .plot_matrix()
            .plot_cluster_labels(
                label_fields=None,
            )
        )
        plotter.show()
        texts = extract_figure_text(plotter._fig, strip=True, nonempty=True)
        assert not texts
    finally:
        plt.show = plt_show


@pytest.mark.api
@pytest.mark.parametrize(
    "label_prefix",
    ["cid", "alpha"],
    ids=["with_cid_prefix", "with_alpha_prefix"],
)
def test_plot_cluster_labels_label_prefix_supports_compressed_labels(
    toy_results,
    label_prefix,
):
    """
    Ensures supported label_prefix modes prefix compressed display labels.

    Args:
        toy_results (Results): Results fixture with clusters and layout.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        plotter = (
            Plotter(toy_results)
            .plot_matrix()
            .plot_cluster_labels(
                label_mode="compressed",
                label_fields=("label", "fe"),
                label_prefix=label_prefix,
                wrap_text=False,
            )
        )
        plotter.show()
        texts = extract_figure_text(plotter._fig, strip=True, nonempty=True)
        tokens = [txt.split(". ", 1)[0] for txt in texts if ". " in txt]
        if label_prefix == "cid":
            assert any(tok.isdigit() for tok in tokens)
        else:
            assert any(tok.isalpha() and tok.isupper() for tok in tokens)
        assert any("FE=" in txt for txt in texts)
    finally:
        plt.show = plt_show


@pytest.mark.api
@pytest.mark.parametrize(
    "label_fields",
    [("label",), ("n", "p"), None],
    ids=["with_label_fields", "np_only", "without_label_fields"],
)
@pytest.mark.parametrize(
    "label_prefix",
    [None, "cid", "alpha"],
    ids=["without_prefix", "with_cid_prefix", "with_alpha_prefix"],
)
def test_plot_cluster_labels_label_prefix_precedence_override_wins(
    toy_results,
    label_fields,
    label_prefix,
):
    """
    Ensures explicit overrides win with and without cid prefix mode.

    Args:
        toy_results (Results): Results fixture with clusters and layout.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        spans = toy_results.cluster_layout().cluster_spans
        assert len(spans) >= 2
        override_cid = int(spans[0][0])
        regular_cid = int(spans[1][0])
        override_prefix = format_label_prefix(label_prefix, override_cid)
        override_label = (
            f"{override_prefix} DNA replication & repair"
            if override_prefix
            else "DNA replication & repair"
        )
        plotter = (
            Plotter(toy_results)
            .plot_matrix()
            .plot_cluster_labels(
                label_fields=label_fields,
                label_prefix=label_prefix,
                overrides={override_cid: override_label},
                wrap_text=False,
            )
        )
        plotter.show()
        texts = extract_figure_text(plotter._fig, strip=True, nonempty=True)
        if label_fields is None:
            assert override_label in texts
        else:
            assert any(txt.startswith(override_label) for txt in texts)
        regular_prefix = format_label_prefix(label_prefix, regular_cid)
        if regular_prefix:
            assert all(txt != f"{regular_prefix} {override_label}" for txt in texts)
            if label_fields is None:
                assert regular_prefix in texts
            else:
                assert any(txt.startswith(f"{regular_prefix} ") for txt in texts)
        else:
            assert all(not txt.startswith(f"{regular_cid}. ") for txt in texts)
    finally:
        plt.show = plt_show


@pytest.mark.api
def test_plot_cluster_labels_accepts_override_mapper(toy_results):
    """
    Ensures cluster label overrides can replace generated labels by cluster id.

    Args:
        toy_results (Results): Results fixture with clusters and layout.

    Raises:
        ValueError: If overrides reference unknown cluster ids.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        # Override one cluster label and verify both explicit and empty-string overrides render safely.
        cid = int(toy_results.cluster_layout().cluster_spans[0][0])
        custom_label = f"Custom-{cid}"
        plotter = (
            Plotter(toy_results)
            .set_label_panel(
                axes=[0.70, 0.05, 0.29, 0.90],
                text_pad=0.01,
            )
            .plot_matrix()
            .plot_cluster_labels(
                overrides={cid: custom_label},
                label_fields=("label",),
            )
        )
        plotter.show()
        fig = plotter._fig
        texts = extract_figure_text(fig)
        assert any(custom_label in t for t in texts)

        # Empty-string override is a valid way to suppress one cluster label and
        # must not break rendering of cluster-level bars.
        plotter2 = (
            Plotter(toy_results)
            .plot_matrix()
            .plot_cluster_labels(overrides={cid: ""}, label_fields=("label",))
            .plot_cluster_bar(name="sig")
        )
        plotter2.show()
        fig2 = plotter2._fig
        patch_counts = [len(ax.patches) for ax in fig2.axes]
        assert max(patch_counts) > 1

        with pytest.raises(TypeError, match="unexpected keyword argument"):
            Plotter(toy_results).plot_cluster_labels(typo_key=True)
    finally:
        plt.show = plt_show


@pytest.mark.api
def test_plot_cluster_labels_placeholder_style_overrides_global_style(toy_results):
    """
    Ensures placeholder labels use placeholder_color/placeholder_alpha over global color/alpha.

    Args:
        toy_results (Results): Results fixture with clusters and layout.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        first_cluster = int(toy_results.cluster_layout().cluster_spans[0][0])
        # Keep layout unchanged but drop one cluster from labels to force placeholder rendering.
        filtered = toy_results.filter(f"cluster == {first_cluster}")
        placeholder_text = "Nonsignificant"
        plotter = (
            Plotter(filtered)
            .plot_matrix()
            .plot_cluster_labels(
                label_fields=("label",),
                skip_unlabeled=False,
                color="gray",
                alpha=0.2,
                placeholder_text=placeholder_text,
                placeholder_color="red",
                placeholder_alpha=1.0,
            )
            .plot_cluster_bar(name="sig")
        )
        plotter.show()

        # Collect rendered labels to validate placeholder-vs-regular style behavior.
        fig = plotter._fig
        texts = [t for ax in fig.axes for t in ax.texts if t.get_text().strip()]
        placeholder_nodes = [t for t in texts if t.get_text() == placeholder_text]
        regular_nodes = [t for t in texts if t.get_text() != placeholder_text]
        assert placeholder_nodes, "Expected placeholder text to be rendered."
        assert regular_nodes, "Expected at least one non-placeholder cluster label."

        # Placeholder text uses placeholder_* style, while labeled clusters keep global color/alpha.
        placeholder_node = placeholder_nodes[0]
        regular_node = regular_nodes[0]
        assert to_rgba(placeholder_node.get_color()) == pytest.approx(to_rgba("red"))
        assert float(placeholder_node.get_alpha()) == pytest.approx(1.0)
        assert to_rgba(regular_node.get_color()) == pytest.approx(to_rgba("gray"))
        assert float(regular_node.get_alpha()) == pytest.approx(0.2)

        # Cluster bars should render for both clusters; unlabeled clusters use min cmap color.
        cmap = plt.get_cmap(plotter._style["sigbar_cmap"])
        min_rgb = to_rgba(cmap(0.0))[:3]
        max_rgb = to_rgba(cmap(1.0))[:3]
        bar_patches = [p for ax in fig.axes for p in ax.patches if p.get_alpha() is not None]
        assert len(bar_patches) == 2, "Expected one sigbar patch per cluster span."
        bar_colors = [to_rgba(p.get_facecolor())[:3] for p in bar_patches]
        assert any(c == pytest.approx(min_rgb) for c in bar_colors)
        assert any(c == pytest.approx(max_rgb) for c in bar_colors)
    finally:
        plt.show = plt_show


@pytest.mark.api
def test_plot_cluster_labels_override_with_np_fields_keeps_label_and_stats(toy_results):
    """
    Ensures override labels remain visible when label_fields excludes 'label', with stable stats order.

    Args:
        toy_results (Results): Results fixture with clusters and layout.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        results_q = toy_results.with_qvalues()
        cid = int(toy_results.cluster_layout().cluster_spans[0][0])
        plotter = (
            Plotter(results_q)
            .plot_matrix()
            .plot_cluster_labels(
                overrides={cid: "CustomLabelVisibleWithFields"},
                label_fields=("n", "q", "fe", "p"),
                wrap_text=False,
            )
        )
        plotter.show()
        fig = plotter._fig
        texts = extract_figure_text(fig)
        custom_texts = [t for t in texts if t.startswith("CustomLabelVisibleWithFields")]
        assert custom_texts, "Expected override label text to remain visible."
        for txt in custom_texts:
            assert "n=" in txt
            assert "$q$=" in txt
            assert "FE=" in txt
            assert "$p$=" in txt
            assert txt.find("n=") < txt.find("$q$=")
            assert txt.find("$q$=") < txt.find("FE=")
            assert txt.find("FE=") < txt.find("$p$=")
    finally:
        plt.show = plt_show


@pytest.mark.unit
def test_plot_cluster_label_override_keeps_deterministic_stats(toy_results):
    """
    Ensures label-only overrides preserve deterministic cluster statistics.

    Args:
        toy_results (Results): Results fixture with clusters and layout.
    """
    # Build baseline labels and assert a text-only override leaves numeric stats unchanged.
    df = toy_results.cluster_labels()
    cid = int(df.loc[0, "cluster"])
    base_pval = float(df.loc[df["cluster"] == cid, "pval"].iloc[0])
    base_fe = float(df.loc[df["cluster"] == cid, "fe"].iloc[0])
    override_map = _parse_label_overrides({cid: f"Custom-{cid}"})
    label_map = _build_label_map(df, override_map)

    assert label_map[cid][0] == f"Custom-{cid}"
    assert float(label_map[cid][1]) == pytest.approx(base_pval)
    assert float(label_map[cid][3]) == pytest.approx(base_pval)
    assert float(label_map[cid][4]) == pytest.approx(base_fe)


@pytest.mark.api
def test_plot_cluster_labels_max_words_controls_label_building_compressed(toy_results):
    """
    Ensures max_words controls compressed label building.

    Args:
        toy_results (Results): Results fixture with clusters and layout.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        # Render compressed labels with max_words=1 and assert generated labels stay single-word.
        plotter = (
            Plotter(toy_results)
            .plot_matrix()
            .plot_cluster_labels(
                label_mode="compressed",
                max_words=1,
                label_fields=("label",),
                wrap_text=False,
            )
        )
        plotter.show()
        fig = plotter._fig
        texts = extract_figure_text(fig, strip=True)
        # Exclude placeholders to focus on generated cluster label text.
        cluster_texts = [t for t in texts if t and t != "—"]
        assert cluster_texts, "Expected generated cluster labels to be rendered."
        for txt in cluster_texts:
            assert len(txt.replace("\n", " ").split()) <= 1
    finally:
        plt.show = plt_show


@pytest.mark.api
def test_plot_cluster_labels_max_words_controls_display(toy_results):
    """
    Ensures max_words truncates rendered override labels without affecting generation settings.

    Args:
        toy_results (Results): Results fixture with clusters and layout.
    """
    plt = use_agg_backend()
    plt_show = plt.show
    plt.show = lambda *args, **kwargs: None
    try:
        # Apply max_words truncation to an override label and verify rendered text is shortened.
        cid = int(toy_results.cluster_layout().cluster_spans[0][0])
        plotter = (
            Plotter(toy_results)
            .plot_matrix()
            .plot_cluster_labels(
                overrides={cid: "Alpha Beta Gamma"},
                label_fields=("label",),
                max_words=1,
                wrap_text=False,
            )
        )
        plotter.show()
        fig = plotter._fig
        texts = extract_figure_text(fig)
        assert any(t.strip() == "Alpha" for t in texts)
        assert not any("Alpha Beta" in t for t in texts)
    finally:
        plt.show = plt_show


@pytest.mark.unit
def test_track_layout_duplicate_names_raise():
    """
    Ensures duplicate track names are rejected at layout time.

    Raises:
        ValueError: If duplicate track names are registered.
    """
    manager = TrackLayoutManager()
    manager.register_track(name="a", renderer=lambda *args, **kwargs: None, width=0.1)
    manager.register_track(name="a", renderer=lambda *args, **kwargs: None, width=0.1)

    with pytest.raises(ValueError):
        manager.compute_layout(base_x=0.0, gutter_width=0.0)


@pytest.mark.unit
def test_track_layout_unknown_order_raises():
    """
    Ensures unknown track names in ordering raise an error.

    Raises:
        ValueError: If ordering includes unknown tracks.
    """
    manager = TrackLayoutManager()
    manager.register_track(name="a", renderer=lambda *args, **kwargs: None, width=0.1)
    manager.set_order(["missing"])

    with pytest.raises(ValueError):
        manager.compute_layout(base_x=0.0, gutter_width=0.0)
