"""
Unit tests for neurodent.constants module.
"""

import pytest
from datetime import datetime
import numpy as np

from neurodent import constants


class TestConstants:
    """Test constants module functionality."""

    def test_default_id_to_lr(self):
        """Test DEFAULT_ID_TO_LR mapping."""
        assert constants.DEFAULT_ID_TO_LR[9] == "L"
        assert constants.DEFAULT_ID_TO_LR[16] == "R"
        assert len(constants.DEFAULT_ID_TO_LR) == 10

    def test_genotype_aliases(self):
        """Test GENOTYPE_ALIASES mapping."""
        assert "WT" in constants.GENOTYPE_ALIASES
        assert "KO" in constants.GENOTYPE_ALIASES
        assert constants.GENOTYPE_ALIASES["WT"] == ["WT", "wildtype"]
        assert constants.GENOTYPE_ALIASES["KO"] == ["KO", "knockout"]

    def test_chname_aliases(self):
        """Test CHNAME_ALIASES mapping."""
        expected_channels = ["Aud", "Vis", "Hip", "Bar", "Mot"]
        for ch in expected_channels:
            assert ch in constants.CHNAME_ALIASES
            assert len(constants.CHNAME_ALIASES[ch]) == 3  # Each has lowercase, uppercase, and ALL_CAPS variants

    def test_lr_aliases(self):
        """Test LR_ALIASES mapping."""
        assert "L" in constants.LR_ALIASES
        assert "R" in constants.LR_ALIASES
        assert "left" in constants.LR_ALIASES["L"]
        assert "right" in constants.LR_ALIASES["R"]

    def test_default_id_to_name(self):
        """Test DEFAULT_ID_TO_NAME mapping."""
        assert constants.DEFAULT_ID_TO_NAME[9] == "LAud"
        assert constants.DEFAULT_ID_TO_NAME[16] == "RMot"
        assert len(constants.DEFAULT_ID_TO_NAME) == 10

    def test_df_sort_order(self):
        """Test DF_SORT_ORDER structure."""
        expected_keys = ["channel", "genotype", "sex", "isday", "band"]
        for key in expected_keys:
            assert key in constants.DF_SORT_ORDER
            assert isinstance(constants.DF_SORT_ORDER[key], list)

    def test_dateparser_patterns(self):
        """Test DATEPARSER_PATTERNS_TO_REMOVE."""
        assert isinstance(constants.DATEPARSER_PATTERNS_TO_REMOVE, list)
        assert len(constants.DATEPARSER_PATTERNS_TO_REMOVE) > 0
        for pattern in constants.DATEPARSER_PATTERNS_TO_REMOVE:
            assert isinstance(pattern, str)

    def test_default_day(self):
        """Test DEFAULT_DAY constant."""
        assert isinstance(constants.DEFAULT_DAY, datetime)
        assert constants.DEFAULT_DAY.year == 2000
        assert constants.DEFAULT_DAY.month == 1
        assert constants.DEFAULT_DAY.day == 1

    def test_global_constants(self):
        """Test global constants."""
        assert constants.GLOBAL_SAMPLING_RATE == 1000
        assert constants.GLOBAL_DTYPE == np.float32

    def test_feature_constants(self):
        """Test feature-related constants."""
        assert isinstance(constants.LINEAR_FEATURES, list)
        assert isinstance(constants.BAND_FEATURES, list)
        assert isinstance(constants.MATRIX_FEATURES, list)
        assert isinstance(constants.HIST_FEATURES, list)
        assert isinstance(constants.FEATURES, list)
        assert isinstance(constants.WAR_FEATURES, list)

        # Check that all feature lists contain expected items
        assert "rms" in constants.LINEAR_FEATURES
        assert "ampvar" in constants.LINEAR_FEATURES
        assert "psdtotal" in constants.LINEAR_FEATURES
        assert "psdslope" in constants.LINEAR_FEATURES
        assert "nspike" in constants.LINEAR_FEATURES
        assert "logrms" in constants.LINEAR_FEATURES
        assert "logampvar" in constants.LINEAR_FEATURES
        assert "logpsdtotal" in constants.LINEAR_FEATURES
        assert "lognspike" in constants.LINEAR_FEATURES
        assert "psdband" in constants.BAND_FEATURES
        assert "psdfrac" in constants.BAND_FEATURES
        assert "logpsdband" in constants.BAND_FEATURES
        assert "logpsdfrac" in constants.BAND_FEATURES
        assert "cohere" in constants.MATRIX_FEATURES
        assert "zcohere" in constants.MATRIX_FEATURES
        assert "imcoh" in constants.MATRIX_FEATURES
        assert "zimcoh" in constants.MATRIX_FEATURES
        assert "pcorr" in constants.MATRIX_FEATURES
        assert "zpcorr" in constants.MATRIX_FEATURES
        assert "psd" in constants.HIST_FEATURES

    def test_feature_plot_height_ratios(self):
        """Test FEATURE_PLOT_HEIGHT_RATIOS for both linear and matrix features."""
        assert isinstance(constants.FEATURE_PLOT_HEIGHT_RATIOS, dict)

        # Test structure and data types
        for feature, ratio in constants.FEATURE_PLOT_HEIGHT_RATIOS.items():
            assert isinstance(feature, str)
            assert isinstance(ratio, (int, float))
            assert ratio > 0

        # Test that both linear and matrix features are included
        linear_features = ["rms", "ampvar", "psdtotal", "psdslope", "psdband", "psdfrac", "nspike"]
        matrix_features = ["cohere", "zcohere", "pcorr", "zpcorr"]

        for feature in linear_features + matrix_features:
            assert feature in constants.FEATURE_PLOT_HEIGHT_RATIOS, f"Missing feature: {feature}"

    def test_freq_bands(self):
        """Test FREQ_BANDS structure."""
        expected_bands = ["delta", "theta", "alpha", "beta", "gamma"]
        for band in expected_bands:
            assert band in constants.FREQ_BANDS
            freq_range = constants.FREQ_BANDS[band]
            assert isinstance(freq_range, tuple)
            assert len(freq_range) == 2
            assert freq_range[0] < freq_range[1]

    def test_band_names(self):
        """Test BAND_NAMES."""
        assert constants.BAND_NAMES == list(constants.FREQ_BANDS.keys())

    def test_freq_constants(self):
        """Test frequency-related constants."""
        assert isinstance(constants.FREQ_BAND_TOTAL, tuple)
        assert len(constants.FREQ_BAND_TOTAL) == 2
        assert constants.FREQ_BAND_TOTAL[0] < constants.FREQ_BAND_TOTAL[1]

        assert isinstance(constants.FREQ_MINS, list)
        assert isinstance(constants.FREQ_MAXS, list)
        assert len(constants.FREQ_MINS) == len(constants.FREQ_MAXS)

        assert constants.LINE_FREQ == 60

    def test_freq_bands_contiguity(self):
        """Test that frequency bands are contiguous without gaps or overlaps."""
        band_items = list(constants.FREQ_BANDS.items())

        # Test contiguity between adjacent bands
        for i in range(len(band_items) - 1):
            current_name, (current_low, current_high) = band_items[i]
            next_name, (next_low, next_high) = band_items[i + 1]

            # Bands should be perfectly contiguous (current_high == next_low)
            assert current_high == next_low, (
                f"Gap/overlap between {current_name} (ends at {current_high}) and {next_name} (starts at {next_low})"
            )

        # Test that combined range matches FREQ_BAND_TOTAL
        combined_range = (band_items[0][1][0], band_items[-1][1][1])
        assert combined_range == constants.FREQ_BAND_TOTAL, (
            f"Combined band range {combined_range} does not match FREQ_BAND_TOTAL {constants.FREQ_BAND_TOTAL}"
        )


class TestOkabeItoColors:
    """Test Okabe-Ito colorblind-friendly color palette."""

    def test_okabe_ito_colors_exists(self):
        """Test that OKABE_ITO_COLORS dictionary exists."""
        assert hasattr(constants, "OKABE_ITO_COLORS")
        assert isinstance(constants.OKABE_ITO_COLORS, dict)

    def test_okabe_ito_colors_count(self):
        """Test that the palette has exactly 8 colors."""
        assert len(constants.OKABE_ITO_COLORS) == 8

    def test_okabe_ito_colors_keys(self):
        """Test that all expected color names are present."""
        expected_colors = ["black", "orange", "blue", "green", "yellow", "lightblue", "red", "purple"]
        assert set(constants.OKABE_ITO_COLORS.keys()) == set(expected_colors)

    def test_okabe_ito_colors_values(self):
        """Test that color values match the reference Okabe-Ito palette."""
        expected_values = {
            "black": "#000000",
            "orange": "#E69F00",
            "blue": "#0072B2",
            "green": "#009E73",
            "yellow": "#F5C710",
            "lightblue": "#56B4E9",
            "red": "#D55E00",
            "purple": "#CC79A7",
        }
        for color_name, expected_hex in expected_values.items():
            assert constants.OKABE_ITO_COLORS[color_name] == expected_hex

    def test_okabe_ito_colors_format(self):
        """Test that all colors are valid hex color strings."""
        for color_name, hex_value in constants.OKABE_ITO_COLORS.items():
            # Should be a string
            assert isinstance(hex_value, str)
            # Should start with #
            assert hex_value.startswith("#")
            # Should be 7 characters long (#RRGGBB)
            assert len(hex_value) == 7
            # Should be valid hex (0-9, A-F)
            assert all(c in "0123456789ABCDEFabcdef#" for c in hex_value)

    def test_colors_not_in_top_level_package(self):
        """Test that colors are NOT exported at package top level."""
        import neurodent

        # Colors should NOT be available at neurodent.blue
        assert not hasattr(neurodent, "blue")
        assert not hasattr(neurodent, "red")
        assert not hasattr(neurodent, "OKABE_ITO_COLORS")

    def test_colors_matplotlib_compatible(self):
        """Test that colors work with matplotlib."""
        import matplotlib.colors as mcolors

        # All colors should be valid matplotlib color specifications
        for color_name, hex_value in constants.OKABE_ITO_COLORS.items():
            assert mcolors.is_color_like(hex_value)

    def test_use_dictionary_for_colors(self):
        """Test that colors can be accessed via OKABE_ITO_COLORS dictionary."""
        # Preferred access pattern
        assert constants.OKABE_ITO_COLORS["black"] == "#000000"
        assert constants.OKABE_ITO_COLORS["orange"] == "#E69F00"
        assert constants.OKABE_ITO_COLORS["blue"] == "#0072B2"
        assert constants.OKABE_ITO_COLORS["green"] == "#009E73"
        assert constants.OKABE_ITO_COLORS["yellow"] == "#F5C710"
        assert constants.OKABE_ITO_COLORS["lightblue"] == "#56B4E9"
        assert constants.OKABE_ITO_COLORS["red"] == "#D55E00"
        assert constants.OKABE_ITO_COLORS["purple"] == "#CC79A7"

