"""
Axis-role resolution: metadata first, names only as a fallback.
"""

import os

import numpy as np
import pytest

from dispnc.conventions import normalize
from dispnc.coords import resolve_variable_axes
from dispnc.io import open_source

from conftest import EXAMPLES, open_sample, sample_files


def roles_of(ds, varname, unstructured=None):
    axes = resolve_variable_axes(ds, varname, unstructured)
    return {a.dim: (a.role, a.source) for a in axes}


def test_xios_resolves_from_axis_attribute(xios_ds):
    ds, _ = normalize(xios_ds)
    got = roles_of(ds, 'ps_avg')
    assert got['lat'] == ('Y', 'axis-attr')
    assert got['lon'] == ('X', 'axis-attr')
    assert got['time_counter'] == ('T', 'axis-attr')


def test_soildepth_resolves_by_positive_not_by_fake_standard_name(xios_ds):
    """
    XIOS writes soildepth:standard_name = "Soil mid-layer depth". The role must
    come from `positive`, and the bogus name must not appear as the source.
    """
    xios_ds['probe'] = (('soildepth', 'lat'), np.zeros((3, 3)))
    ds, _ = normalize(xios_ds)
    axis = resolve_variable_axes(ds, 'probe').by_dim('soildepth')
    assert axis.role == 'Z'
    assert axis.source in ('axis-attr', 'positive')
    assert axis.positive == 'down'
    # the sentence XIOS put in standard_name must not be what decided the role
    assert 'standard_name' not in ds['soildepth'].attrs


def test_pem_resolves_after_normalization(pem_ds):
    ds, _ = normalize(pem_ds)
    got = roles_of(ds, 'tsurf')
    assert got['latitude'][0] == 'Y'
    assert got['longitude'][0] == 'X'
    assert got['Time'][0] == 'T'


def test_staggered_rlonu_gets_a_role(lmdz_ds):
    """
    rlonu/rlatv match no name table in the original code and rendered as
    meaningless axes. After normalization they resolve as real longitudes.
    """
    ds, _ = normalize(lmdz_ds)
    axis = resolve_variable_axes(ds, 'ucov').by_dim('rlonu')
    assert axis.role == 'X'
    assert axis.units == 'degrees_east'


def test_meaningless_dimensions_are_S():
    import xarray as xr
    ds = xr.Dataset({'v': (('nslope', 'index'), np.zeros((2, 3)))})
    got = roles_of(ds, 'v')
    assert got['nslope'][0] == 'S'
    assert got['index'][0] == 'S'


def test_duplicate_roles_are_broken_by_confidence():
    """
    Two dimensions must never both be X: the less confident one is demoted.
    """
    import xarray as xr
    ds = xr.Dataset(
        {'v': (('lon', 'rlonu'), np.zeros((3, 4)))},
        coords={'lon': ('lon', [0.0, 1, 2], {'axis': 'X'}),
                'rlonu': ('rlonu', [0.0, 1, 2, 3], {})},
    )
    axes = resolve_variable_axes(ds, 'v')
    assert [a.role for a in axes].count('X') == 1
    assert axes.by_dim('lon').role == 'X'
    assert axes.by_dim('rlonu').role == 'S'


def test_cyclic_longitude_detected(xios_ds):
    """
    XIOS longitudes run -180..168.75 in 11.25 degree steps: they wrap the globe
    even though the last point is not 180.
    """
    import xarray as xr
    lon = np.arange(-180, 180, 11.25)
    ds = xr.Dataset({'v': (('lon',), np.zeros(lon.size))},
                    coords={'lon': ('lon', lon, {'axis': 'X', 'units': 'degrees_east'})})
    assert resolve_variable_axes(ds, 'v').x.is_cyclic


def test_non_cyclic_longitude_subset():
    import xarray as xr
    lon = np.arange(0, 90, 10.0)
    ds = xr.Dataset({'v': (('lon',), np.zeros(lon.size))},
                    coords={'lon': ('lon', lon, {'axis': 'X', 'units': 'degrees_east'})})
    assert not resolve_variable_axes(ds, 'v').x.is_cyclic


# --- the real files -----------------------------------------------------------
#
# Two kinds of test read sample_files/. The table below names files and
# variables, and each case skips when its own file or variable is not in the
# sample set of the day. The sweep after it names nothing: it asserts what must
# hold of any file the tool can be pointed at, so it keeps working when the
# samples are replaced. Prefer adding to the sweep; the table is for the
# specific quirks worth pinning down.

REAL_CASES = [
    ('diagevo.nc',           'tsurf',     {'Time': 'T', 'latitude': 'Y', 'longitude': 'X'}),
    ('diagfi.nc',            'phisinit',  {'latitude': 'Y', 'longitude': 'X'}),
    ('xoutyearly4pem_y1.nc', 'ps_avg',    {'time_counter': 'T', 'lat': 'Y', 'lon': 'X'}),
    ('xoutyearly4pem_y1.nc', 'tsoil_avg', {'time_counter': 'T', 'soildepth': 'Z',
                                           'lat': 'Y', 'lon': 'X'}),
    ('start.nc',             'ucov',      {'Time': 'T', 'altitude': 'Z',
                                           'latitude': 'Y', 'rlonu': 'X'}),
    ('startfi.nc',           'tsurf',     {'physical_points': 'U'}),
    ('startevo.nc',          'tsoil',     {'Time': 'T', 'nslope': 'S',
                                           'subsurface_layers': 'Z',
                                           'physical_points': 'U'}),
]


@pytest.mark.parametrize('filename,varname,expected', REAL_CASES)
def test_real_files_resolve_every_dimension(filename, varname, expected):
    with open_sample(filename, varname) as (ds, report):
        axes = resolve_variable_axes(ds, varname, report.unstructured)
        got = {a.dim: a.role for a in axes}
        assert got == expected


def test_ucov_x_axis_is_the_staggered_one():
    """
    start.nc's ucov lives on rlonu, not on longitude. Resolving it as X is what
    makes the plot meaningful.
    """
    with open_sample('start.nc', 'ucov') as (ds, report):
        axes = resolve_variable_axes(ds, 'ucov', report.unstructured)
        assert axes.x.dim == 'rlonu'
        assert axes.x.units == 'degrees_east'
        # 185.625 degrees: past 180, which is why the radian test needs a 2*pi
        # bound rather than the pi bound the original code used
        assert axes.x.values.max() == pytest.approx(185.625, abs=1e-3)


# One case per sample file, whatever they happen to be, and a readable skip
# rather than pytest's "empty parameter set" when there are none.
SWEEP = sample_files() or [pytest.param(
    None, id='none',
    marks=pytest.mark.skip(reason=f"no .nc files in {EXAMPLES}; see README.md"))]


@pytest.mark.parametrize('path', SWEEP, ids=os.path.basename)
def test_every_variable_of_every_sample_resolves(path):
    """
    Whatever the sample set holds, every variable of every file must resolve to
    exactly one axis per dimension, with no role claimed twice - a file that
    resolved `lat` and `latitude` both as Y would plot nonsense. This is the
    test that survives the samples being replaced.
    """
    src = open_source(path)
    try:
        ds, report = normalize(src.ds)
        for varname in ds.variables:
            axes = resolve_variable_axes(ds, varname, report.unstructured)
            dims = list(ds[varname].dims)
            assert [a.dim for a in axes] == dims, f"{varname}: axes do not match dims"
            claimed = [a.role for a in axes if a.role != 'S']
            assert len(claimed) == len(set(claimed)), \
                f"{varname}: role claimed twice ({claimed})"
    finally:
        src.close()
