import os

import numpy as np
import pandas as pd
import pytest
from numpy.testing import assert_allclose

from statsmodels.tsa.seasonal import STL

cur_dir = os.path.dirname(os.path.abspath(__file__))
file_path = os.path.join(cur_dir, 'results', 'stl_test_results.csv')
results = pd.read_csv(file_path)
results.columns = [c.strip() for c in results.columns]
results.scenario = results.scenario.apply(str.strip)
results = results.set_index(['scenario', 'idx'])


@pytest.fixture(scope='module', params=[True, False])
def robust(request):
    return request.param


def default_kwargs_base():
    file_path = os.path.join(cur_dir, 'results', 'stl_co2.csv')
    co2 = np.asarray(pd.read_csv(file_path, header=None).iloc[:, 0])
    y = co2
    nobs = y.shape[0]
    nperiod = 12
    work = np.zeros((nobs + 2 * nperiod, 7))
    rw = np.ones(nobs)
    trend = np.zeros(nobs)
    season = np.zeros(nobs)
    return dict(y=y, n=y.shape[0], np=nperiod, ns=35, nt=19, nl=13, no=2, ni=1,
                nsjump=4, ntjump=2, nljump=2, isdeg=1, itdeg=1, ildeg=1, rw=rw,
                trend=trend, season=season, work=work)


@pytest.fixture(scope='function')
def default_kwargs():
    return default_kwargs_base()


@pytest.fixture(scope='function')
def default_kwargs_short():
    kwargs = default_kwargs_base()
    y = kwargs['y'][:-1]
    nobs = y.shape[0]
    work = np.zeros((nobs + 2 * kwargs['np'], 7))
    rw = np.ones(nobs)
    trend = np.zeros(nobs)
    season = np.zeros(nobs)
    kwargs.update(dict(y=y, n=nobs, rw=rw, trend=trend,
                       season=season, work=work))
    return kwargs


def _to_class_kwargs(kwargs, robust=False):
    endog = kwargs['y']
    np = kwargs['np']
    ns = kwargs['ns']
    nt = kwargs['nt']
    nl = kwargs['nl']
    isdeg = kwargs['isdeg']
    itdeg = kwargs['itdeg']
    ildeg = kwargs['ildeg']
    nsjump = kwargs['nsjump']
    ntjump = kwargs['ntjump']
    nljump = kwargs['nljump']
    outer_iter = kwargs['no']
    inner_iter = kwargs['ni']
    class_kwargs = dict(endog=endog, period=np, seasonal=ns, trend=nt,
                        low_pass=nl,
                        seasonal_deg=isdeg, trend_deg=itdeg,
                        low_pass_deg=ildeg,
                        robust=robust, seasonal_jump=nsjump, trend_jump=ntjump,
                        low_pass_jump=nljump)
    return class_kwargs, outer_iter, inner_iter


def test_baseline_class(default_kwargs):
    class_kwargs, outer, inner = _to_class_kwargs(default_kwargs)
    mod = STL(**class_kwargs)
    res = mod.fit(outer_iter=outer, inner_iter=inner)

    expected = results.loc['baseline'].sort_index()
    assert_allclose(res.trend, expected.trend)
    assert_allclose(res.seasonal, expected.season)
    assert_allclose(res.weights, expected.rw)
    resid = class_kwargs['endog'] - expected.trend - expected.season
    assert_allclose(res.resid, resid)


def test_short_class(default_kwargs_short):
    class_kwargs, outer, inner = _to_class_kwargs(default_kwargs_short)
    mod = STL(**class_kwargs)
    res = mod.fit(outer_iter=outer, inner_iter=inner)

    expected = results.loc['short'].sort_index()
    assert_allclose(res.seasonal, expected.season)
    assert_allclose(res.trend, expected.trend)
    assert_allclose(res.weights, expected.rw)


def test_nljump_1_class(default_kwargs):
    default_kwargs['nljump'] = 1
    class_kwargs, outer, inner = _to_class_kwargs(default_kwargs)
    mod = STL(**class_kwargs)
    res = mod.fit(outer_iter=outer, inner_iter=inner)

    expected = results.loc['nljump-1'].sort_index()
    assert_allclose(res.seasonal, expected.season)
    assert_allclose(res.trend, expected.trend)
    assert_allclose(res.weights, expected.rw)


def test_ntjump_1_class(default_kwargs):
    default_kwargs['ntjump'] = 1
    class_kwargs, outer, inner = _to_class_kwargs(default_kwargs)
    mod = STL(**class_kwargs)
    res = mod.fit(outer_iter=outer, inner_iter=inner)

    expected = results.loc['ntjump-1'].sort_index()
    assert_allclose(res.seasonal, expected.season)
    assert_allclose(res.trend, expected.trend)
    assert_allclose(res.weights, expected.rw)


def test_nljump_1_ntjump_1_class(default_kwargs):
    default_kwargs['nljump'] = 1
    default_kwargs['ntjump'] = 1
    class_kwargs, outer, inner = _to_class_kwargs(default_kwargs)
    mod = STL(**class_kwargs)
    res = mod.fit(outer_iter=outer, inner_iter=inner)

    expected = results.loc['nljump-1-ntjump-1'].sort_index()
    assert_allclose(res.seasonal, expected.season)
    assert_allclose(res.trend, expected.trend)
    assert_allclose(res.weights, expected.rw)


def test_parameter_checks_period(default_kwargs):
    class_kwargs, _, _ = _to_class_kwargs(default_kwargs)
    endog = class_kwargs['endog']
    endog2 = np.hstack((endog[:, None], endog[:, None]))
    period = class_kwargs['period']
    with pytest.raises(ValueError, match='y must be a 1d array'):
        STL(endog=endog2, period=period)
    match = 'period must be a positive integer >= 2'
    with pytest.raises(ValueError, match=match):
        STL(endog=endog, period=1)
    with pytest.raises(ValueError, match=match):
        STL(endog=endog, period=-12)
    with pytest.raises(ValueError, match=match):
        STL(endog=endog, period=4.0)


def test_parameter_checks_seasonal(default_kwargs):
    class_kwargs, _, _ = _to_class_kwargs(default_kwargs)
    endog = class_kwargs['endog']
    period = class_kwargs['period']
    match = 'seasonal must be an odd positive integer >= 3'
    with pytest.raises(ValueError, match=match):
        STL(endog=endog, period=period, seasonal=2)
    with pytest.raises(ValueError, match=match):
        STL(endog=endog, period=period, seasonal=-7)
    with pytest.raises(ValueError, match=match):
        STL(endog=endog, period=period, seasonal=13.0)


def test_parameter_checks_trend(default_kwargs):
    class_kwargs, _, _ = _to_class_kwargs(default_kwargs)
    endog = class_kwargs['endog']
    period = class_kwargs['period']
    match = 'trend must be an odd positive integer >= 3 where trend > period'
    with pytest.raises(ValueError, match=match):
        STL(endog=endog, period=period, trend=14)
    with pytest.raises(ValueError, match=match):
        STL(endog=endog, period=period, trend=11)
    with pytest.raises(ValueError, match=match):
        STL(endog=endog, period=period, trend=-19)
    with pytest.raises(ValueError, match=match):
        STL(endog=endog, period=period, trend=19.0)


def test_parameter_checks_low_pass(default_kwargs):
    class_kwargs, _, _ = _to_class_kwargs(default_kwargs)
    endog = class_kwargs['endog']
    period = class_kwargs['period']

    match = 'low_pass must be an odd positive integer >= 3 where' \
            ' low_pass > period'
    with pytest.raises(ValueError, match=match):
        STL(endog=endog, period=period, low_pass=14)
    with pytest.raises(ValueError, match=match):
        STL(endog=endog, period=period, low_pass=7)
    with pytest.raises(ValueError, match=match):
        STL(endog=endog, period=period, low_pass=-19)
    with pytest.raises(ValueError, match=match):
        STL(endog=endog, period=period, low_pass=19.0)


def test_jump_errors(default_kwargs):
    class_kwargs, _, _ = _to_class_kwargs(default_kwargs)
    endog = class_kwargs['endog']
    period = class_kwargs['period']
    with pytest.raises(ValueError, match='low_pass_jump must be a positve'):
        STL(endog=endog, period=period, low_pass_jump=0)
    with pytest.raises(ValueError, match='low_pass_jump must be a positve'):
        STL(endog=endog, period=period, low_pass_jump=1.0)
    with pytest.raises(ValueError, match='seasonal_jump must be a positve'):
        STL(endog=endog, period=period, seasonal_jump=0)
    with pytest.raises(ValueError, match='seasonal_jump must be a positve'):
        STL(endog=endog, period=period, seasonal_jump=1.0)
    with pytest.raises(ValueError, match='trend_jump must be a positve'):
        STL(endog=endog, period=period, trend_jump=0)
    with pytest.raises(ValueError, match='trend_jump must be a positve'):
        STL(endog=endog, period=period, trend_jump=1.0)


def test_defaults_smoke(default_kwargs, robust):
    class_kwargs, _, _ = _to_class_kwargs(default_kwargs, robust)
    endog = class_kwargs['endog']
    period = class_kwargs['period']
    mod = STL(endog=endog, period=period)
    mod.fit()


def test_pandas(default_kwargs, robust):
    class_kwargs, _, _ = _to_class_kwargs(default_kwargs, robust)
    endog = pd.Series(class_kwargs['endog'], name='y')
    period = class_kwargs['period']
    mod = STL(endog=endog, period=period)
    res = mod.fit()
    assert isinstance(res.trend, pd.Series)
    assert isinstance(res.seasonal, pd.Series)
    assert isinstance(res.resid, pd.Series)
    assert isinstance(res.weights, pd.Series)


def test_period_detection(default_kwargs):
    class_kwargs, _, _ = _to_class_kwargs(default_kwargs)
    mod = STL(**class_kwargs)
    res = mod.fit()

    del class_kwargs['period']
    endog = class_kwargs['endog']
    index = pd.date_range('1-1-1959', periods=348, freq='M')
    class_kwargs['endog'] = pd.Series(endog, index=index)
    mod = STL(**class_kwargs)

    res_implicit_period = mod.fit()
    assert_allclose(res.seasonal, res_implicit_period.seasonal)


def test_no_period(default_kwargs):
    class_kwargs, _, _ = _to_class_kwargs(default_kwargs)
    del class_kwargs['period']
    class_kwargs['endog'] = pd.Series(class_kwargs['endog'])
    with pytest.raises(ValueError, match='Unable to determine period from'):
        STL(**class_kwargs)


@pytest.mark.matplotlib
def test_plot(default_kwargs):
    class_kwargs, outer, inner = _to_class_kwargs(default_kwargs)
    res = STL(**class_kwargs).fit(outer_iter=outer, inner_iter=inner)
    res.plot()

    class_kwargs['endog'] = pd.Series(class_kwargs['endog'], name='CO2')
    res = STL(**class_kwargs).fit()
    res.plot()


def test_default_trend(default_kwargs):
    # GH 6686
    class_kwargs, _, _ = _to_class_kwargs(default_kwargs)
    class_kwargs["seasonal"] = 17
    class_kwargs["trend"] = None
    mod = STL(**class_kwargs)
    period = class_kwargs["period"]
    seasonal = class_kwargs["seasonal"]
    expected = int(np.ceil(1.5 * period / (1 - 1.5 / seasonal)))
    expected += 1 if expected % 2 == 0 else 0
    assert mod.config["trend"] == expected

    class_kwargs["seasonal"] = 7
    mod = STL(**class_kwargs)
    period = class_kwargs["period"]
    seasonal = class_kwargs["seasonal"]
    expected = int(np.ceil(1.5 * period / (1 - 1.5 / seasonal)))
    expected += 1 if expected % 2 == 0 else 0
    assert mod.config["trend"] == expected
