from pathlib import Path
import platform
import warnings

import pandas as pd
import os
from pkg_resources import parse_version
import pytest
from functools import wraps

import pvlib
from pvlib.location import Location

pvlib_base_version = \
    parse_version(parse_version(pvlib.__version__).base_version)


# decorator takes one argument: the base version for which it should fail
# for example @fail_on_pvlib_version('0.7') will cause a test to fail
# on pvlib versions 0.7a, 0.7b, 0.7rc1, etc.
def fail_on_pvlib_version(version):
    # second level of decorator takes the function under consideration
    def wrapper(func):
        # third level defers computation until the test is called
        # this allows the specific test to fail at test runtime,
        # rather than at decoration time (when the module is imported)
        @wraps(func)
        def inner(*args, **kwargs):
            # fail if the version is too high
            if pvlib_base_version >= parse_version(version):
                pytest.fail('the tested function is scheduled to be '
                            'removed in %s' % version)
            # otherwise return the function to be executed
            else:
                return func(*args, **kwargs)
        return inner
    return wrapper


def _check_pandas_assert_kwargs(kwargs):
    # handles the change in API related to default
    # tolerances in pandas 1.1.0.  See pvlib GH #1018
    if parse_version(pd.__version__) >= parse_version('1.1.0'):
        if kwargs.pop('check_less_precise', False):
            kwargs['atol'] = 1e-3
            kwargs['rtol'] = 1e-3
        else:
            kwargs['atol'] = 1e-5
            kwargs['rtol'] = 1e-5
    else:
        kwargs.pop('rtol', None)
        kwargs.pop('atol', None)
    return kwargs


def assert_index_equal(left, right, **kwargs):
    kwargs = _check_pandas_assert_kwargs(kwargs)
    pd.testing.assert_index_equal(left, right, **kwargs)


def assert_series_equal(left, right, **kwargs):
    kwargs = _check_pandas_assert_kwargs(kwargs)
    pd.testing.assert_series_equal(left, right, **kwargs)


def assert_frame_equal(left, right, **kwargs):
    kwargs = _check_pandas_assert_kwargs(kwargs)
    pd.testing.assert_frame_equal(left, right, **kwargs)


# commonly used directories in the tests
TEST_DIR = Path(__file__).parent
DATA_DIR = TEST_DIR.parent / 'data'


# pytest-rerunfailures variables
RERUNS = 5
RERUNS_DELAY = 2


platform_is_windows = platform.system() == 'Windows'
skip_windows = pytest.mark.skipif(platform_is_windows,
                                  reason='does not run on windows')


try:
    # Attempt to load BSRN credentials used for testing pvlib.iotools.get_bsrn
    bsrn_username = os.environ["BSRN_FTP_USERNAME"]
    bsrn_password = os.environ["BSRN_FTP_PASSWORD"]
    has_bsrn_credentials = True
except KeyError:
    has_bsrn_credentials = False

requires_bsrn_credentials = pytest.mark.skipif(
    not has_bsrn_credentials, reason='requires bsrn credentials')


try:
    import statsmodels  # noqa: F401
    has_statsmodels = True
except ImportError:
    has_statsmodels = False

requires_statsmodels = pytest.mark.skipif(
    not has_statsmodels, reason='requires statsmodels')


try:
    import ephem
    has_ephem = True
except ImportError:
    has_ephem = False

requires_ephem = pytest.mark.skipif(not has_ephem, reason='requires ephem')


def has_spa_c():
    try:
        from pvlib.spa_c_files.spa_py import spa_calc
    except ImportError:
        return False
    else:
        return True


requires_spa_c = pytest.mark.skipif(not has_spa_c(), reason="requires spa_c")


def has_numba():
    try:
        import numba
    except ImportError:
        return False
    else:
        vers = numba.__version__.split('.')
        if int(vers[0] + vers[1]) < 17:
            return False
        else:
            return True


requires_numba = pytest.mark.skipif(not has_numba(), reason="requires numba")

try:
    import siphon
    has_siphon = True
except ImportError:
    has_siphon = False

requires_siphon = pytest.mark.skipif(not has_siphon,
                                     reason='requires siphon')

try:
    import netCDF4  # noqa: F401
    has_netCDF4 = True
except ImportError:
    has_netCDF4 = False

requires_netCDF4 = pytest.mark.skipif(not has_netCDF4,
                                      reason='requires netCDF4')

try:
    import pvfactors  # noqa: F401
    has_pvfactors = True
except ImportError:
    has_pvfactors = False

requires_pvfactors = pytest.mark.skipif(not has_pvfactors,
                                        reason='requires pvfactors')


try:
    import PySAM  # noqa: F401
    has_pysam = True
except ImportError:
    has_pysam = False

requires_pysam = pytest.mark.skipif(not has_pysam, reason="requires PySAM")


try:
    import cftime  # noqa: F401

    has_recent_cftime = parse_version(cftime.__version__) > parse_version(
        "1.1.0"
    )
except ImportError:
    has_recent_cftime = False

requires_recent_cftime = pytest.mark.skipif(
    not has_recent_cftime, reason="requires cftime > 1.1.0"
)


@pytest.fixture()
def golden():
    return Location(39.742476, -105.1786, 'America/Denver', 1830.14)


@pytest.fixture()
def golden_mst():
    return Location(39.742476, -105.1786, 'MST', 1830.14)


@pytest.fixture()
def expected_solpos():
    return pd.DataFrame({'elevation': 39.872046,
                         'apparent_zenith': 50.111622,
                         'azimuth': 194.340241,
                         'apparent_elevation': 39.888378},
                        index=['2003-10-17T12:30:30Z'])


@pytest.fixture(scope="session")
def sam_data():
    data = {}
    with warnings.catch_warnings():
        # ignore messages about duplicate entries in the databases.
        warnings.simplefilter("ignore", UserWarning)
        data['sandiamod'] = pvlib.pvsystem.retrieve_sam('sandiamod')
        data['adrinverter'] = pvlib.pvsystem.retrieve_sam('adrinverter')
    return data


@pytest.fixture(scope="function")
def pvsyst_module_params():
    """
    Define some PVSyst module parameters for testing.

    The scope of the fixture is set to ``'function'`` to allow tests to modify
    parameters if required without affecting other tests.
    """
    parameters = {
        'gamma_ref': 1.05,
        'mu_gamma': 0.001,
        'I_L_ref': 6.0,
        'I_o_ref': 5e-9,
        'EgRef': 1.121,
        'R_sh_ref': 300,
        'R_sh_0': 1000,
        'R_s': 0.5,
        'R_sh_exp': 5.5,
        'cells_in_series': 60,
        'alpha_sc': 0.001,
    }
    return parameters


@pytest.fixture(scope='function')
def adr_inverter_parameters():
    """
    Define some ADR inverter parameters for testing.

    The scope of the fixture is set to ``'function'`` to allow tests to modify
    parameters if required without affecting other tests.
    """
    parameters = {
        'Name': 'Ablerex Electronics Co., Ltd.: ES 2200-US-240 (240Vac)'
                '[CEC 2011]',
        'Vac': 240.,
        'Pacmax': 2110.,
        'Pnom': 2200.,
        'Vnom': 396.,
        'Vmin': 155.,
        'Vmax': 413.,
        'Vdcmax': 500.,
        'MPPTHi': 450.,
        'MPPTLow': 150.,
        'Pnt': 0.25,
        'ADRCoefficients': [0.01385, 0.0152, 0.00794, 0.00286, -0.01872,
                            -0.01305, 0.0, 0.0, 0.0]
    }
    return parameters


@pytest.fixture(scope='function')
def cec_inverter_parameters():
    """
    Define some CEC inverter parameters for testing.

    The scope of the fixture is set to ``'function'`` to allow tests to modify
    parameters if required without affecting other tests.
    """
    parameters = {
        'Name': 'ABB: MICRO-0.25-I-OUTD-US-208 208V [CEC 2014]',
        'Vac': 208.0,
        'Paco': 250.0,
        'Pdco': 259.5220505,
        'Vdco': 40.24260317,
        'Pso': 1.771614224,
        'C0': -2.48e-5,
        'C1': -9.01e-5,
        'C2': 6.69e-4,
        'C3': -0.0189,
        'Pnt': 0.02,
        'Vdcmax': 65.0,
        'Idcmax': 10.0,
        'Mppt_low': 20.0,
        'Mppt_high': 50.0,
    }
    return parameters


@pytest.fixture(scope='function')
def cec_module_params():
    """
    Define some CEC module parameters for testing.

    The scope of the fixture is set to ``'function'`` to allow tests to modify
    parameters if required without affecting other tests.
    """
    parameters = {
        'Name': 'Example Module',
        'BIPV': 'Y',
        'Date': '4/28/2008',
        'T_NOCT': 65,
        'A_c': 0.67,
        'N_s': 18,
        'I_sc_ref': 7.5,
        'V_oc_ref': 10.4,
        'I_mp_ref': 6.6,
        'V_mp_ref': 8.4,
        'alpha_sc': 0.003,
        'beta_oc': -0.04,
        'a_ref': 0.473,
        'I_L_ref': 7.545,
        'I_o_ref': 1.94e-09,
        'R_s': 0.094,
        'R_sh_ref': 15.72,
        'Adjust': 10.6,
        'gamma_r': -0.5,
        'Version': 'MM105',
        'PTC': 48.9,
        'Technology': 'Multi-c-Si',
    }
    return parameters


@pytest.fixture(scope='function')
def cec_module_cs5p_220m():
    """
    Define Canadian Solar CS5P-220M module parameters for testing.

    The scope of the fixture is set to ``'function'`` to allow tests to modify
    parameters if required without affecting other tests.
    """
    parameters = {
        'Name': 'Canadian Solar CS5P-220M',
        'BIPV': 'N',
        'Date': '10/5/2009',
        'T_NOCT': 42.4,
        'A_c': 1.7,
        'N_s': 96,
        'I_sc_ref': 5.1,
        'V_oc_ref': 59.4,
        'I_mp_ref': 4.69,
        'V_mp_ref': 46.9,
        'alpha_sc': 0.004539,
        'beta_oc': -0.22216,
        'a_ref': 2.6373,
        'I_L_ref': 5.114,
        'I_o_ref': 8.196e-10,
        'R_s': 1.065,
        'R_sh_ref': 381.68,
        'Adjust': 8.7,
        'gamma_r': -0.476,
        'Version': 'MM106',
        'PTC': 200.1,
        'Technology': 'Mono-c-Si',
    }
    return parameters


@pytest.fixture(scope='function')
def cec_module_spr_e20_327():
    """
    Define SunPower SPR-E20-327 module parameters for testing.

    The scope of the fixture is set to ``'function'`` to allow tests to modify
    parameters if required without affecting other tests.
    """
    parameters = {
        'Name': 'SunPower SPR-E20-327',
        'BIPV': 'N',
        'Date': '1/14/2013',
        'T_NOCT': 46,
        'A_c': 1.631,
        'N_s': 96,
        'I_sc_ref': 6.46,
        'V_oc_ref': 65.1,
        'I_mp_ref': 5.98,
        'V_mp_ref': 54.7,
        'alpha_sc': 0.004522,
        'beta_oc': -0.23176,
        'a_ref': 2.6868,
        'I_L_ref': 6.468,
        'I_o_ref': 1.88e-10,
        'R_s': 0.37,
        'R_sh_ref': 298.13,
        'Adjust': -0.1862,
        'gamma_r': -0.386,
        'Version': 'NRELv1',
        'PTC': 301.4,
        'Technology': 'Mono-c-Si',
    }
    return parameters


@pytest.fixture(scope='function')
def cec_module_fs_495():
    """
    Define First Solar FS-495 module parameters for testing.

    The scope of the fixture is set to ``'function'`` to allow tests to modify
    parameters if required without affecting other tests.
    """
    parameters = {
        'Name': 'First Solar FS-495',
        'BIPV': 'N',
        'Date': '9/18/2014',
        'T_NOCT': 44.6,
        'A_c': 0.72,
        'N_s': 216,
        'I_sc_ref': 1.55,
        'V_oc_ref': 86.5,
        'I_mp_ref': 1.4,
        'V_mp_ref': 67.9,
        'alpha_sc': 0.000924,
        'beta_oc': -0.22741,
        'a_ref': 2.9482,
        'I_L_ref': 1.563,
        'I_o_ref': 2.64e-13,
        'R_s': 6.804,
        'R_sh_ref': 806.27,
        'Adjust': -10.65,
        'gamma_r': -0.264,
        'Version': 'NRELv1',
        'PTC': 89.7,
        'Technology': 'CdTe',
    }
    return parameters


@pytest.fixture(scope='function')
def sapm_temperature_cs5p_220m():
    # SAPM temperature model parameters for Canadian_Solar_CS5P_220M
    # (glass/polymer) in open rack
    return {'a': -3.40641, 'b': -0.0842075, 'deltaT': 3}


@pytest.fixture(scope='function')
def sapm_module_params():
    """
    Define SAPM model parameters for Canadian Solar CS5P 220M module.

    The scope of the fixture is set to ``'function'`` to allow tests to modify
    parameters if required without affecting other tests.
    """
    parameters = {'Material': 'c-Si',
                  'Cells_in_Series': 96,
                  'Parallel_Strings': 1,
                  'A0': 0.928385,
                  'A1': 0.068093,
                  'A2': -0.0157738,
                  'A3': 0.0016606,
                  'A4': -6.93E-05,
                  'B0': 1,
                  'B1': -0.002438,
                  'B2': 0.0003103,
                  'B3': -0.00001246,
                  'B4': 2.11E-07,
                  'B5': -1.36E-09,
                  'C0': 1.01284,
                  'C1': -0.0128398,
                  'C2': 0.279317,
                  'C3': -7.24463,
                  'C4': 0.996446,
                  'C5': 0.003554,
                  'C6': 1.15535,
                  'C7': -0.155353,
                  'Isco': 5.09115,
                  'Impo': 4.54629,
                  'Voco': 59.2608,
                  'Vmpo': 48.3156,
                  'Aisc': 0.000397,
                  'Aimp': 0.000181,
                  'Bvoco': -0.21696,
                  'Mbvoc': 0.0,
                  'Bvmpo': -0.235488,
                  'Mbvmp': 0.0,
                  'N': 1.4032,
                  'IXO': 4.97599,
                  'IXXO': 3.18803,
                  'FD': 1}
    return parameters
