import matplotlib

matplotlib.use("Agg")

import matplotlib.pyplot as plt
import pytest
from matplotlib.colors import Colormap

import plotstyle as ps
from plotstyle import colors


def test_categorical_palette_matches_kit_hex():
    assert colors.CATEGORICAL == [
        "#009682",
        "#DF9B1B",
        "#4664AA",
        "#A78230",
        "#23A1E0",
        "#A3107C",
        "#8CB63C",
        "#A22223",
        "#FCE500",
    ]


def test_diverging_palette_matches_kit_hex():
    assert colors.DIVERGING == {"low": "#4664AA", "mid": "#f7f7f7", "high": "#A22223"}


def test_status_palette_matches_validated_hex():
    assert colors.STATUS == {
        "good": "#0ca30c",
        "warning": "#fab219",
        "serious": "#ec835a",
        "critical": "#d03b3b",
    }


def test_get_color_returns_categorical_slot():
    assert colors.get_color(0) == colors.CATEGORICAL[0]
    assert colors.get_color(len(colors.CATEGORICAL) - 1) == colors.CATEGORICAL[-1]


def test_get_color_out_of_range_raises():
    with pytest.raises(ValueError):
        colors.get_color(len(colors.CATEGORICAL))


def test_sequential_steps_are_kit_blue_variations():
    assert colors.SEQUENTIAL_STEPS[-1].lower() == "#4664aa"
    # Lightest step should be a lighter (higher-lightness) tint than the brand color.
    import colorsys

    def lightness(hex_color):
        r, g, b = (int(hex_color.lstrip("#")[i : i + 2], 16) / 255 for i in (0, 2, 4))
        return colorsys.rgb_to_hls(r, g, b)[1]

    assert lightness(colors.SEQUENTIAL_STEPS[0]) > lightness(colors.SEQUENTIAL_STEPS[-1])


def test_sequential_cmap_is_usable_colormap():
    cmap = colors.sequential_cmap()
    assert isinstance(cmap, Colormap)
    assert cmap(0.0) != cmap(1.0)


def test_diverging_cmap_is_usable_colormap():
    cmap = colors.diverging_cmap()
    assert isinstance(cmap, Colormap)
    assert cmap(0.0) != cmap(1.0)


def test_use_sets_expected_rcparams():
    ps.use()
    try:
        assert plt.rcParams["axes.facecolor"] == "#ffffff"
        assert plt.rcParams["text.usetex"] is True
        assert "lmodern" in plt.rcParams["text.latex.preamble"]
        assert "sfmath" in plt.rcParams["text.latex.preamble"]
        assert "fontenc" in plt.rcParams["text.latex.preamble"]
        assert r"\sfdefault" in plt.rcParams["text.latex.preamble"]
        cycle_colors = [entry["color"] for entry in plt.rcParams["axes.prop_cycle"]]
        assert cycle_colors == colors.CATEGORICAL

        assert plt.rcParams["axes.spines.top"] is False
        assert plt.rcParams["axes.spines.right"] is False
        assert plt.rcParams["axes.spines.left"] is False
        assert plt.rcParams["axes.spines.bottom"] is True
        assert plt.rcParams["xtick.bottom"] is True
        assert plt.rcParams["ytick.left"] is True
        assert plt.rcParams["axes.edgecolor"] == "#000000"
        assert plt.rcParams["axes.linewidth"] > 0.8

        assert plt.rcParams["axes.grid"] is True
        assert plt.rcParams["axes.grid.axis"] == "y"
        assert plt.rcParams["axes.titlelocation"] == "left"

        assert plt.rcParams["xtick.minor.visible"] is True
        assert plt.rcParams["ytick.minor.visible"] is False
        assert plt.rcParams["xtick.major.size"] > plt.rcParams["xtick.minor.size"]
    finally:
        ps.reset()


def test_use_default_does_not_cycle_linestyles():
    ps.use()
    try:
        cycle_keys = plt.rcParams["axes.prop_cycle"].keys
        assert cycle_keys == {"color"}
    finally:
        ps.reset()


def test_use_cycle_linestyles_opt_in():
    ps.use(cycle_linestyles=True)
    try:
        cycle_keys = plt.rcParams["axes.prop_cycle"].keys
        assert cycle_keys == {"color", "linestyle"}
        linestyles = {entry["linestyle"] for entry in plt.rcParams["axes.prop_cycle"]}
        assert len(linestyles) > 1
    finally:
        ps.reset()


def test_new_figure_styles_major_and_minor_tick_labels_differently():
    fig, ax = ps.new_figure("square")
    try:
        assert ax.xaxis.get_tick_params(which="major")["labelcolor"] == colors.INK["primary"]
        assert ax.xaxis.get_tick_params(which="minor")["labelcolor"] == colors.INK["muted"]
        assert ax.yaxis.get_tick_params(which="major")["labelcolor"] == colors.INK["primary"]
        assert ax.yaxis.get_tick_params(which="minor")["labelcolor"] == colors.INK["muted"]
    finally:
        plt.close(fig)


def test_new_figure_preset_sizes():
    fig, ax = ps.new_figure("thesis-single")
    try:
        assert tuple(fig.get_size_inches()) == ps.FIGSIZES["thesis-single"]
    finally:
        plt.close(fig)


def test_new_figure_explicit_tuple():
    fig, ax = ps.new_figure((3.0, 2.0))
    try:
        assert tuple(fig.get_size_inches()) == (3.0, 2.0)
    finally:
        plt.close(fig)


def test_new_figure_unknown_preset_raises():
    with pytest.raises(ValueError):
        ps.new_figure("not-a-real-preset")


def test_new_figure_title_sets_left_aligned_figure_suptitle():
    fig, ax = ps.new_figure("square", title="My Title")
    try:
        assert fig._suptitle is not None
        assert "My Title" in fig._suptitle.get_text()
        assert fig._suptitle.get_ha() == "left"
        assert ax.get_title() == ""
    finally:
        plt.close(fig)


def test_new_figure_params_adds_subtitle_line():
    fig, ax = ps.new_figure("square", title="My Title", params={"N": 100, "seed": 42})
    try:
        text = fig._suptitle.get_text()
        assert "My Title" in text
        assert "N: 100" in text
        assert "seed: 42" in text
        assert "|" in text
    finally:
        plt.close(fig)


def test_new_figure_without_title_or_params_has_no_suptitle():
    fig, ax = ps.new_figure("square")
    try:
        assert fig._suptitle is None
    finally:
        plt.close(fig)


def test_colorbar_matches_axes_height():
    fig, ax = ps.new_figure("square")
    try:
        im = ax.imshow([[0, 1], [2, 3]])
        cb = ps.colorbar(im, ax)
        fig.canvas.draw()
        ax_bbox = ax.get_position()
        cax_bbox = cb.ax.get_position()
        assert ax_bbox.y0 == pytest.approx(cax_bbox.y0, abs=1e-6)
        assert ax_bbox.y1 == pytest.approx(cax_bbox.y1, abs=1e-6)
    finally:
        plt.close(fig)


def test_colorbar_has_no_outline():
    fig, ax = ps.new_figure("square")
    try:
        im = ax.imshow([[0, 1], [2, 3]])
        cb = ps.colorbar(im, ax)
        assert cb.outline.get_visible() is False
    finally:
        plt.close(fig)


def test_no_spines_hides_all_spines_on_single_axes():
    fig, ax = ps.new_figure("square")
    try:
        ax.imshow([[0, 1], [2, 3]])
        ps.no_spines(ax)
        assert all(not spine.get_visible() for spine in ax.spines.values())
    finally:
        plt.close(fig)


def test_no_spines_hides_all_spines_on_axes_array():
    fig, axes = ps.new_figure("square", nrows=1, ncols=2)
    try:
        ps.no_spines(axes)
        for ax in axes:
            assert all(not spine.get_visible() for spine in ax.spines.values())
    finally:
        plt.close(fig)


def test_savefig_writes_requested_formats(tmp_path):
    fig, ax = ps.new_figure("square")
    ax.plot([0, 1], [0, 1])
    try:
        out_dir = tmp_path / "nested" / "plots"
        written = ps.savefig(fig, out_dir / "my_plot", formats=("pdf", "png"))
    finally:
        plt.close(fig)

    assert [p.name for p in written] == ["my_plot.pdf", "my_plot.png"]
    for p in written:
        assert p.exists()
        assert p.stat().st_size > 0


def test_style_legend_places_legend_on_figure_outside_axes():
    fig, ax = ps.new_figure("square")
    try:
        ax.plot([0, 1], [0, 1], label="series")
        legend = ps.style_legend(ax, title="Series")
        assert legend in fig.legends
        assert ax.get_legend() is None
        assert legend.get_title().get_text() == "Series"
    finally:
        plt.close(fig)


def test_style_legend_without_title_warns():
    fig, ax = ps.new_figure("square")
    try:
        ax.plot([0, 1], [0, 1], label="series")
        with pytest.warns(UserWarning):
            ps.style_legend(ax)
    finally:
        plt.close(fig)


def test_panel_label_does_not_raise():
    fig, ax = ps.new_figure("square")
    try:
        ax.plot([0, 1], [0, 1], label="series")
        ps.panel_label(ax, "a")
    finally:
        plt.close(fig)


def test_panel_label_unknown_loc_raises():
    fig, ax = ps.new_figure("square")
    try:
        with pytest.raises(ValueError):
            ps.panel_label(ax, "a", loc="middle")
    finally:
        plt.close(fig)


def test_panel_label_defaults_to_lower_right():
    fig, ax = ps.new_figure("square")
    try:
        text = ps.panel_label(ax, "a")
        assert text.get_ha() == "right"
        assert text.get_va() == "bottom"
        x, y = text.get_position()
        assert x > 0.5
        assert y < 0.5
    finally:
        plt.close(fig)


def test_panel_label_color_matches_xlabel():
    fig, ax = ps.new_figure("square")
    try:
        ax.set_xlabel("Time (s)")
        text = ps.panel_label(ax, "a")
        assert text.get_color() == ax.xaxis.label.get_color()
    finally:
        plt.close(fig)


def test_panel_label_has_rounded_semi_transparent_box_by_default():
    fig, ax = ps.new_figure("square")
    try:
        text = ps.panel_label(ax, "a")
        patch = text.get_bbox_patch()
        assert patch is not None
        assert "round" in patch.get_boxstyle().__class__.__name__.lower()
        assert patch.get_facecolor()[3] < 1.0
        assert patch.get_edgecolor()[3] == 1.0
    finally:
        plt.close(fig)


def test_panel_label_box_can_be_disabled():
    fig, ax = ps.new_figure("square")
    try:
        text = ps.panel_label(ax, "a", box=False)
        assert text.get_bbox_patch() is None
    finally:
        plt.close(fig)
