"""Unit tests for json_builder module."""
import json
import tempfile
from pathlib import Path

import pytest

from utils.json_builder import (
    build_config,
    build_and_save_config,
    _parse_list,
    _parse_float_list,
)


# -----------------------------------------------------------------------------
# _parse_list
# -----------------------------------------------------------------------------


class TestParseList:
    """Unit tests for _parse_list."""

    def test_empty_string_returns_none(self):
        assert _parse_list("") is None
        assert _parse_list("   ") is None

    def test_whitespace_only_returns_none(self):
        assert _parse_list(" \t\n ") is None

    def test_single_item(self):
        assert _parse_list("foo") == ["foo"]
        assert _parse_list("  foo  ") == ["foo"]

    def test_multiple_items_comma_separated(self):
        assert _parse_list("a, b, c") == ["a", "b", "c"]
        assert _parse_list("a,b,c") == ["a", "b", "c"]

    def test_strips_whitespace_around_items(self):
        assert _parse_list("  a  ,  b  ,  c  ") == ["a", "b", "c"]

    def test_skips_empty_parts(self):
        assert _parse_list("a,,b") == ["a", "b"]
        assert _parse_list(",a,b,") == ["a", "b"]

    def test_none_like_input(self):
        assert _parse_list(None) is None

    def test_non_string_coerced(self):
        assert _parse_list(123) == ["123"]


# -----------------------------------------------------------------------------
# _parse_float_list
# -----------------------------------------------------------------------------


class TestParseFloatList:
    """Unit tests for _parse_float_list."""

    def test_empty_state_returns_none(self):
        assert _parse_float_list({}) is None

    def test_missing_yaxis_lim_returns_none(self):
        assert _parse_float_list({"other": "value"}) is None

    def test_empty_yaxis_lim_returns_none(self):
        assert _parse_float_list({"yaxis_lim": ""}) is None
        assert _parse_float_list({"yaxis_lim": "   "}) is None

    def test_single_value_returns_none(self):
        assert _parse_float_list({"yaxis_lim": "1.0"}) is None

    def test_three_values_returns_none(self):
        assert _parse_float_list({"yaxis_lim": "1.0, 2.0, 3.0"}) is None

    def test_valid_two_floats(self):
        assert _parse_float_list({"yaxis_lim": "0, 10"}) == [0.0, 10.0]
        assert _parse_float_list({"yaxis_lim": "-1.5, 2.5"}) == [-1.5, 2.5]
        assert _parse_float_list({"yaxis_lim": "  -1.5  ,  2.5  "}) == [-1.5, 2.5]

    def test_invalid_values_returns_none(self):
        assert _parse_float_list({"yaxis_lim": "foo, bar"}) is None
        assert _parse_float_list({"yaxis_lim": "1.0, bar"}) is None
        assert _parse_float_list({"yaxis_lim": "1.0,"}) is None


# -----------------------------------------------------------------------------
# build_config
# -----------------------------------------------------------------------------


class TestBuildConfig:
    """Unit tests for build_config with various state inputs."""

    def test_minimal_state_single_field(self):
        """Minimal config with one field and empty state."""
        cfg = build_config(
            fields=["/path/to/file.nc"],
            figure="time_series",
            state={},
            output_file="/out/figure.png",
        )
        assert cfg["figure"] == "time_series"
        assert cfg["input_file"] == "/path/to/file.nc"
        assert cfg["output_file"] == "/out/figure.png"
        assert cfg["grid_type"] == "lonlat"
        assert cfg["dark_mode"] is False
        assert cfg["transparent"] is False
        assert cfg["x_fig"] == 10
        assert cfg["y_fig"] == 10

    def test_minimal_state_multiple_fields(self):
        """Multiple fields produce list input_file."""
        cfg = build_config(
            fields=["a.nc", "b.nc"],
            figure="map_diff",
            state={},
            output_file="/out/figure.png",
        )
        assert cfg["input_file"] == ["a.nc", "b.nc"]

    def test_preprocess_options(self):
        """File format, source, region, grid_type."""
        state = {
            "file_format": "netcdf",
            "source": "my_source",
            "region": "Europe",
            "region_type": "country",
            "grid_type": "unstructured",
        }
        cfg = build_config(
            fields=["x.nc"],
            figure="time_series",
            state=state,
            output_file="/out.png",
        )
        assert cfg["file_format"] == "netcdf"
        assert cfg["source"] == "my_source"
        assert cfg["region"] == "Europe"
        assert cfg["region_type"] == "country"
        assert cfg["grid_type"] == "unstructured"

    def test_time_slice(self):
        """Time slice when both bounds set."""
        state = {"time_slice_start": 0, "time_slice_end": 10}
        cfg = build_config(
            fields=["x.nc"],
            figure="time_series",
            state=state,
            output_file="/out.png",
        )
        assert cfg["time_slice"] == [0, 10]

    def test_time_slice_invalid_skipped(self):
        """Invalid time slice values are skipped."""
        state = {"time_slice_start": "foo", "time_slice_end": 10}
        cfg = build_config(
            fields=["x.nc"],
            figure="time_series",
            state=state,
            output_file="/out.png",
        )
        assert "time_slice" not in cfg

    def test_time_slice_partial_skipped(self):
        """Only one bound set -> no time_slice."""
        state = {"time_slice_start": 0}
        cfg = build_config(
            fields=["x.nc"],
            figure="time_series",
            state=state,
            output_file="/out.png",
        )
        assert "time_slice" not in cfg

    def test_lat_lon_slices(self):
        """Lat and lon slices."""
        state = {
            "lat_slice_min": -90.0,
            "lat_slice_max": 90.0,
            "lon_slice_min": 0.0,
            "lon_slice_max": 360.0,
        }
        cfg = build_config(
            fields=["x.nc"],
            figure="time_series",
            state=state,
            output_file="/out.png",
        )
        assert cfg["lat_slice"] == [-90.0, 90.0]
        assert cfg["lon_slice"] == [0.0, 360.0]

    def test_convert_units(self):
        """Convert units: factor, units."""
        state = {"convert_units": "1e-12, GtC"}
        cfg = build_config(
            fields=["x.nc"],
            figure="time_series",
            state=state,
            output_file="/out.png",
        )
        assert cfg["convert_units"] == [1e-12, "GtC"]

    def test_convert_units_invalid_skipped(self):
        """Invalid convert_units format skipped."""
        state = {"convert_units": "not-valid"}
        cfg = build_config(
            fields=["x.nc"],
            figure="time_series",
            state=state,
            output_file="/out.png",
        )
        assert "convert_units" not in cfg

    def test_apply_op(self):
        """Apply op with operand and optional layers."""
        state = {"apply_op": "mul", "apply_operand": 2.5}
        cfg = build_config(
            fields=["x.nc"],
            figure="time_series",
            state=state,
            output_file="/out.png",
        )
        assert cfg["apply"] == {"op": "mul", "operand": 2.5}

    def test_apply_op_with_layers(self):
        state = {
            "apply_op": "mul",
            "apply_operand": 2.5,
            "apply_layers": "layer1, layer2",
        }
        cfg = build_config(
            fields=["x.nc"],
            figure="time_series",
            state=state,
            output_file="/out.png",
        )
        assert cfg["apply"] == {"op": "mul", "operand": 2.5, "layers": "layer1, layer2"}

    def test_common_figure_options(self):
        """Title, palette, dark_mode, transparent, size."""
        state = {
            "title": "My Title",
            "palette": "viridis",
            "dark_mode": True,
            "transparent": True,
            "x_fig": 12,
            "y_fig": 8,
        }
        cfg = build_config(
            fields=["x.nc"],
            figure="time_series",
            state=state,
            output_file="/out.png",
        )
        assert cfg["title"] == "My Title"
        assert cfg["palette"] == "viridis"
        assert cfg["dark_mode"] is True
        assert cfg["transparent"] is True
        assert cfg["x_fig"] == 12
        assert cfg["y_fig"] == 8

    def test_custom_palette_overrides_palette(self):
        """custom_palette takes precedence over palette."""
        state = {"palette": "viridis", "custom_palette": "#ff0000,#00ff00"}
        cfg = build_config(
            fields=["x.nc"],
            figure="time_series",
            state=state,
            output_file="/out.png",
        )
        assert cfg["custom_palette"] == "#ff0000,#00ff00"
        assert "palette" not in cfg

    def test_map_simple_options(self):
        """Map-specific options for map_simple."""
        state = {
            "layer": "temperature",
            "timeop": "sum",
            "cb_label": "Temp",
            "unit": "K",
            "n_classes": 6,
            "classification": "quantile",
            "orientation": "vertical",
            "extend": "both",
            "proj": "PlateCarree",
            "force_zero": True,
            "stats_annotation": True,
        }
        cfg = build_config(
            fields=["x.nc"],
            figure="map_simple",
            state=state,
            output_file="/out.png",
        )
        assert cfg["layer"] == "temperature"
        assert cfg["timeop"] == "sum"
        assert cfg["cb_label"] == "Temp"
        assert cfg["unit"] == "K"
        assert cfg["n_classes"] == 6
        assert cfg["classification"] == "quantile"
        assert cfg["orientation"] == "vertical"
        assert cfg["extend"] == "both"
        assert cfg["proj"] == "PlateCarree"
        assert cfg["force_zero"] is True
        assert cfg["stats_annotation"] is True

    def test_map_diff_extra_options(self):
        """map_diff adds percentage and nonsig."""
        state = {"layer": "var", "percentage": True, "nonsig": True}
        cfg = build_config(
            fields=["a.nc", "b.nc"],
            figure="map_diff",
            state=state,
            output_file="/out.png",
        )
        assert cfg["percentage"] is True
        assert cfg["nonsig"] is True

    def test_time_series_layers_single(self):
        """Time series: single layer becomes string."""
        state = {"layers": "layer1"}
        cfg = build_config(
            fields=["x.nc"],
            figure="time_series",
            state=state,
            output_file="/out.png",
        )
        assert cfg["layers"] == "layer1"

    def test_time_series_layers_multiple(self):
        """Time series: multiple layers become list."""
        state = {"layers": "layer1, layer2, layer3"}
        cfg = build_config(
            fields=["x.nc"],
            figure="time_series",
            state=state,
            output_file="/out.png",
        )
        assert cfg["layers"] == ["layer1", "layer2", "layer3"]

    def test_time_series_yaxis_lim(self):
        """Time series: yaxis_lim parsed to floats."""
        state = {"yaxis_lim": "0, 100"}
        cfg = build_config(
            fields=["x.nc"],
            figure="time_series",
            state=state,
            output_file="/out.png",
        )
        assert cfg["yaxis_lim"] == [0.0, 100.0]

    def test_time_series_field_labels(self):
        """Time series: field_labels parsed from comma-separated."""
        state = {"field_labels": "Label A, Label B"}
        cfg = build_config(
            fields=["a.nc", "b.nc"],
            figure="time_series",
            state=state,
            output_file="/out.png",
        )
        assert cfg["field_labels"] == ["Label A", "Label B"]

    def test_static_plot_options(self):
        """Static scatter plot options."""
        state = {
            "field_a_label": "Field A",
            "field_b_label": "Field B",
            "unit_a": "kg",
            "unit_b": "m",
            "scatter_size": 10,
            "scatter_alpha": 0.5,
        }
        cfg = build_config(
            fields=["a.nc", "b.nc"],
            figure="static",
            state=state,
            output_file="/out.png",
        )
        assert cfg["field_a_label"] == "Field A"
        assert cfg["field_b_label"] == "Field B"
        assert cfg["unit_a"] == "kg"
        assert cfg["unit_b"] == "m"
        assert cfg["scatter_size"] == 10
        assert cfg["scatter_alpha"] == 0.5

    def test_distribution_options(self):
        """Distribution plot options."""
        state = {
            "plot_type": "violin",
            "gridop": "av",
            "horizontal": True,
            "vertical_xlabels": True,
        }
        cfg = build_config(
            fields=["x.nc"],
            figure="distribution",
            state=state,
            output_file="/out.png",
        )
        assert cfg["plot_type"] == "violin"
        assert cfg["gridop"] == "av"
        assert cfg["horizontal"] is True
        assert cfg["vertical_xlabels"] is True

    def test_latitudinal_plot_options(self):
        """Latitudinal plot options."""
        state = {"yaxis_label": "Latitude", "legend_style": "compact"}
        cfg = build_config(
            fields=["x.nc"],
            figure="latitudinal_plot",
            state=state,
            output_file="/out.png",
        )
        assert cfg["yaxis_label"] == "Latitude"
        assert cfg["legend_style"] == "compact"


# -----------------------------------------------------------------------------
# build_and_save_config
# -----------------------------------------------------------------------------


class TestBuildAndSaveConfig:
    """Unit tests for build_and_save_config."""

    def test_writes_valid_json(self):
        """Config is written as valid JSON."""
        with tempfile.TemporaryDirectory() as tmpdir:
            json_path, output_path = build_and_save_config(
                fields=["/data/file.nc"],
                figure="time_series",
                state={"title": "Test"},
                temp_dir=tmpdir,
            )
            assert json_path == str(Path(tmpdir) / "config.json")
            assert output_path == str(Path(tmpdir) / "figure.png")
            with open(json_path) as f:
                cfg = json.load(f)
            assert cfg["figure"] == "time_series"
            assert cfg["input_file"] == "/data/file.nc"
            assert cfg["title"] == "Test"
