"""
Two variables in one colour, decodably.

Blending puts two variables in one colour by accident and the result belongs to
neither bar. This does it on purpose, with a key that names every combination -
which is the whole difference, so most of what matters here is that the scheme
and the key agree.
"""

import numpy as np
import pytest

from dispnc.bivariate import CLASSES, classify, colours, square


def test_a_field_is_cut_into_as_many_classes_as_the_square_has():
    binned = classify(np.linspace(0, 100, 300))
    assert set(np.unique(binned)) == set(range(CLASSES))


def test_the_classes_are_quantiles_not_even_intervals():
    """
    These fields are routinely spread over decades. Cutting the *range* into
    three would put almost every cell in the bottom class and leave two thirds
    of the square unused; cutting the *population* into three fills it.
    """
    spread = np.logspace(0, 6, 300)

    counts = [int((classify(spread) == c).sum()) for c in range(CLASSES)]
    assert max(counts) - min(counts) <= 1, 'quantiles split the population evenly'

    # what even intervals would have done to the same field, for contrast
    edges = np.linspace(spread.min(), spread.max(), CLASSES + 1)[1:-1]
    even = [int((np.digitize(spread, edges) == c).sum()) for c in range(CLASSES)]
    assert max(even) > 0.9 * spread.size, 'this is the failure being avoided'


def test_cells_with_nothing_in_them_are_marked_apart():
    binned = classify(np.array([1.0, np.nan, 3.0]))
    assert binned[1] == -1


def test_a_flat_field_does_not_get_an_arbitrary_split():
    binned = classify(np.ones(50))
    assert len(set(np.unique(binned))) == 1


def test_the_square_has_one_colour_per_combination():
    key = square()
    assert key.shape == (CLASSES, CLASSES, 3)
    flat = key.reshape(-1, 3)
    assert len({tuple(np.round(c, 4)) for c in flat}) == CLASSES * CLASSES


def test_the_corners_of_the_square_are_the_four_cases():
    key = square()
    low_low = key[0, 0]
    high_x = key[0, -1]
    high_y = key[-1, 0]
    high_both = key[-1, -1]
    # low-low is the palest, high-both the darkest: an empty region reads empty
    assert low_low.sum() > high_x.sum() and low_low.sum() > high_y.sum()
    assert high_both.sum() < low_low.sum()
    # and the two single-variable corners are genuinely different hues
    assert np.argmax(high_x) != np.argmax(high_y)


def test_the_image_is_rgb_the_hover_readout_already_understands():
    """
    `figure.attach_format_coord` handles (M, N, 3) arrays, so the cursor
    readout works over a bivariate map without anything further.
    """
    rgb = colours(np.random.default_rng(0).random((6, 8)),
                  np.random.default_rng(1).random((6, 8)))
    assert rgb.shape == (6, 8, 3)
    assert rgb.min() >= 0.0 and rgb.max() <= 1.0


def test_a_cell_missing_from_either_field_is_left_blank():
    """
    A hole in one variable must not take its colour from the other alone.
    """
    x = np.array([[1.0, 2.0]])
    y = np.array([[np.nan, 2.0]])
    rgb = colours(x, y)
    assert np.allclose(rgb[0, 0], 1.0), 'a missing half has to read as missing'
    assert not np.allclose(rgb[0, 1], 1.0)


def test_the_image_agrees_with_the_key():
    """
    The map and its legend have to be the same scheme, or the key is decoration.
    """
    x = np.array([[0.0, 100.0]])
    y = np.array([[0.0, 100.0]])
    rgb = colours(x, y)
    key = square()
    assert np.allclose(rgb[0, 0], key[0, 0])
    assert np.allclose(rgb[0, 1], key[-1, -1])
