"""
Ensure that layers and their convenience methods on the viewer
have the same signatures and docstrings.
"""

import inspect
import re
from unittest.mock import MagicMock, call

import numpy as np
import pytest
from numpydoc.docscrape import ClassDoc, FunctionDoc

import napari
from napari import Viewer
from napari import layers as module
from napari._tests.utils import check_viewer_functioning, layer_test_data
from napari.utils.misc import camel_to_snake

layers = []

for name in dir(module):
    obj = getattr(module, name)

    if obj is module.Layer or not inspect.isclass(obj):
        continue

    if issubclass(obj, module.Layer):
        layers.append(obj)


@pytest.mark.parametrize('layer', layers, ids=lambda layer: layer.__name__)
def test_docstring(layer):
    name = layer.__name__

    method_name = f'add_{camel_to_snake(name)}'
    method = getattr(Viewer, method_name)

    method_doc = FunctionDoc(method)
    layer_doc = ClassDoc(layer)

    # check summary section
    method_summary = ' '.join(method_doc['Summary'])  # join multi-line summary

    summary_format = 'Add an? .+? layer to the layer list.'

    assert re.match(
        summary_format, method_summary
    ), f"improper 'Summary' section of '{method_name}'"

    # check parameters section
    method_params = method_doc['Parameters']
    layer_params = layer_doc['Parameters']

    # Remove path parameter from viewer method if it exists
    method_params = [m for m in method_params if m.name != 'path']

    if name == 'Image':
        # For Image just test arguments that are in layer are in method
        named_method_params = [m.name for m in method_params]
        for layer_param in layer_params:
            l_name, l_type, l_description = layer_param
            assert l_name in named_method_params
    else:
        try:
            assert len(method_params) == len(layer_params)
            for method_param, layer_param in zip(method_params, layer_params):
                m_name, m_type, m_description = method_param
                l_name, l_type, l_description = layer_param

                # descriptions are treated as lists where each line is an
                # element
                m_description = ' '.join(m_description)
                l_description = ' '.join(l_description)

                assert m_name == l_name, 'different parameter names or order'
                assert (
                    m_type == l_type
                ), f"type mismatch of parameter '{m_name}'"
                assert (
                    m_description == l_description
                ), f"description mismatch of parameter '{m_name}'"
        except AssertionError as e:
            raise AssertionError(
                f"docstrings don't match for class {name}"
            ) from e

    # check returns section
    (method_returns,) = method_doc[
        'Returns'
    ]  # only one thing should be returned
    description = ' '.join(method_returns[-1])  # join multi-line description
    method_returns = *method_returns[:-1], description

    if name == 'Image':
        assert method_returns == (
            'layer',
            f':class:`napari.layers.{name}` or list',
            f'The newly-created {name.lower()} layer or list of {name.lower()} layers.',  # noqa: E501
        ), f"improper 'Returns' section of '{method_name}'"
    else:
        assert method_returns == (
            'layer',
            f':class:`napari.layers.{name}`',
            f'The newly-created {name.lower()} layer.',
        ), f"improper 'Returns' section of '{method_name}'"


@pytest.mark.parametrize('layer', layers, ids=lambda layer: layer.__name__)
def test_signature(layer):
    name = layer.__name__

    method = getattr(Viewer, f'add_{camel_to_snake(name)}')

    class_parameters = dict(inspect.signature(layer.__init__).parameters)
    method_parameters = dict(inspect.signature(method).parameters)

    fail_msg = f"signatures don't match for class {name}"
    if name == 'Image':
        # If Image just test that class params appear in method
        for class_param in class_parameters.keys():
            assert class_param in method_parameters.keys(), fail_msg
    else:
        assert class_parameters == method_parameters, fail_msg


@pytest.mark.parametrize('layer_type, data, ndim', layer_test_data)
def test_view(qtbot, layer_type, data, ndim):
    np.random.seed(0)
    viewer = getattr(napari, f'view_{layer_type.__name__.lower()}')(
        data, show=False
    )
    view = viewer.window.qt_viewer
    check_viewer_functioning(viewer, view, data, ndim)
    viewer.close()


def test_view_multichannel(qtbot):
    """Test adding image."""
    np.random.seed(0)
    data = np.random.random((15, 10, 5))
    viewer = napari.view_image(data, channel_axis=-1, show=False)
    assert len(viewer.layers) == data.shape[-1]
    for i in range(data.shape[-1]):
        assert np.all(viewer.layers[i].data == data.take(i, axis=-1))
    viewer.close()


def test_kwargs_passed(monkeypatch):
    viewer_mock = MagicMock(napari.Viewer)
    monkeypatch.setattr(napari, 'Viewer', viewer_mock)
    napari.view_path(
        path='some/path', title='my viewer', name='img name', scale=(1, 2, 3)
    )
    assert viewer_mock.mock_calls == [
        call(
            title='my viewer',
            ndisplay=2,
            order=(),
            axis_labels=(),
            show=True,
        ),
        call().open(
            path='some/path',
            stack=False,
            plugin=None,
            layer_type=None,
            name='img name',
            scale=(1, 2, 3),
        ),
    ]
