"""
Tests for SARIMAX models

Author: Chad Fulton
License: Simplified-BSD
"""
from statsmodels.compat.pandas import PD_LT_2
from statsmodels.compat.platform import PLATFORM_WIN

import os
import warnings

import numpy as np
from numpy.testing import assert_, assert_allclose, assert_almost_equal, assert_equal
import pandas as pd
import pytest

import statsmodels.iolib.summary
from statsmodels.tools import add_constant
from statsmodels.tools.sm_exceptions import ValueWarning
from statsmodels.tools.tools import Bunch
from statsmodels.tsa.statespace import sarimax, tools

from .results import results_sarimax

current_path = os.path.dirname(os.path.abspath(__file__))

realgdp_path = os.path.join("results", "results_realgdpar_stata.csv")
realgdp_results = pd.read_csv(current_path + os.sep + realgdp_path)

coverage_path = os.path.join("results", "results_sarimax_coverage.csv")
coverage_results = pd.read_csv(os.path.join(current_path, coverage_path))


class TestSARIMAXStatsmodels:
    """
    Test ARIMA model using SARIMAX class against statsmodels ARIMA class

    Notes
    -----

    Standard errors are quite good for the OPG case.
    """

    @classmethod
    def setup_class(cls):
        cls.true = results_sarimax.wpi1_stationary
        endog = cls.true["data"]
        # Old results from statsmodels.arima.ARIMA taken before it was removed
        # to let test continue to run. On old statsmodels, can run
        # result_a = arima.ARIMA(endog, order=(1, 1, 1)).fit(disp=-1)
        result_a = Bunch()
        result_a.llf = -135.3513139733829
        result_a.aic = 278.7026279467658
        result_a.bic = 289.9513653682555
        result_a.hqic = 283.27183681851653
        result_a.params = np.array([0.74982449, 0.87421135, -0.41202195])
        result_a.bse = np.array([0.29207409, 0.06377779, 0.12208469])
        cls.result_a = result_a
        cls.model_b = sarimax.SARIMAX(
            endog,
            order=(1, 1, 1),
            trend="c",
            simple_differencing=True,
            hamilton_representation=True,
        )
        cls.result_b = cls.model_b.fit(disp=-1)

    def test_loglike(self):
        assert_allclose(self.result_b.llf, self.result_a.llf)

    def test_aic(self):
        assert_allclose(self.result_b.aic, self.result_a.aic)

    def test_bic(self):
        assert_allclose(self.result_b.bic, self.result_a.bic)

    def test_hqic(self):
        assert_allclose(self.result_b.hqic, self.result_a.hqic)

    def test_mle(self):
        # ARIMA estimates the mean of the process, whereas SARIMAX estimates
        # the intercept. Convert the mean to intercept to compare
        params_a = self.result_a.params.copy()
        params_a[0] = (1 - params_a[1]) * params_a[0]
        assert_allclose(self.result_b.params[:-1], params_a, atol=5e-5)

    def test_bse(self):
        # Test the complex step approximated BSE values
        cpa = self.result_b._cov_params_approx(approx_complex_step=True)
        bse = cpa.diagonal() ** 0.5
        assert_allclose(bse[1:-1], self.result_a.bse[1:], atol=1e-5)

    def test_t_test(self):
        import statsmodels.tools._testing as smt

        # to trigger failure, un-comment the following:
        #  self.result_b._cache['pvalues'] += 1
        smt.check_ttest_tvalues(self.result_b)
        smt.check_ftest_pvalues(self.result_b)


class TestRealGDPARStata:
    """
    Includes tests of filtered states and standardized forecast errors.

    Notes
    -----
    Could also test the usual things like standard errors, etc. but those are
    well-tested elsewhere.
    """

    @classmethod
    def setup_class(cls):
        dlgdp = np.log(realgdp_results["value"]).diff()[1:].values
        cls.model = sarimax.SARIMAX(
            dlgdp, order=(12, 0, 0), trend="n", hamilton_representation=True
        )
        # Estimated by Stata
        params = [
            0.40725515,
            0.18782621,
            -0.01514009,
            -0.01027267,
            -0.03642297,
            0.11576416,
            0.02573029,
            -0.00766572,
            0.13506498,
            0.08649569,
            0.06942822,
            -0.10685783,
            0.00007999607,
        ]
        cls.results = cls.model.filter(params)

    def test_filtered_state(self):
        for i in range(12):
            assert_allclose(
                realgdp_results.iloc[1:]["u%d" % (i + 1)],
                self.results.filter_results.filtered_state[i],
                atol=1e-6,
            )

    def test_standardized_forecasts_error(self):
        assert_allclose(
            realgdp_results.iloc[1:]["rstd"],
            self.results.filter_results.standardized_forecasts_error[0],
            atol=1e-3,
        )


class SARIMAXStataTests:
    def test_loglike(self):
        assert_almost_equal(self.result.llf, self.true["loglike"], 4)

    def test_aic(self):
        assert_almost_equal(self.result.aic, self.true["aic"], 3)

    def test_bic(self):
        assert_almost_equal(self.result.bic, self.true["bic"], 3)

    def test_hqic(self):
        hqic = (
            -2 * self.result.llf
            + 2
            * np.log(np.log(self.result.nobs_effective))
            * self.result.params.shape[0]
        )
        assert_almost_equal(self.result.hqic, hqic, 3)

    def test_standardized_forecasts_error(self):
        cython_sfe = self.result.standardized_forecasts_error
        self.result._standardized_forecasts_error = None
        python_sfe = self.result.standardized_forecasts_error
        assert_allclose(cython_sfe, python_sfe)


class ARIMA(SARIMAXStataTests):
    """
    ARIMA model

    Stata arima documentation, Example 1
    """

    @classmethod
    def setup_class(cls, true, *args, **kwargs):
        cls.true = true
        endog = true["data"]

        kwargs.setdefault("simple_differencing", True)
        kwargs.setdefault("hamilton_representation", True)

        cls.model = sarimax.SARIMAX(endog, *args, order=(1, 1, 1), trend="c", **kwargs)

        # Stata estimates the mean of the process, whereas SARIMAX estimates
        # the intercept of the process. Get the intercept.
        intercept = (1 - true["params_ar"][0]) * true["params_mean"][0]
        params = np.r_[
            intercept, true["params_ar"], true["params_ma"], true["params_variance"]
        ]

        cls.result = cls.model.filter(params)

    def test_mle(self):
        result = self.model.fit(disp=-1)
        assert_allclose(result.params, self.result.params, atol=1e-3)


class TestARIMAStationary(ARIMA):
    """
    Notes
    -----

    Standard errors are very good for the OPG and complex step approximation
    cases.
    """

    @classmethod
    def setup_class(cls):
        super().setup_class(results_sarimax.wpi1_stationary)

    def test_bse(self):
        # test defaults
        assert_equal(self.result.cov_type, "opg")
        assert_equal(self.result._cov_approx_complex_step, True)
        assert_equal(self.result._cov_approx_centered, False)
        # default covariance type (opg)
        assert_allclose(self.result.bse[1], self.true["se_ar_opg"], atol=1e-7)
        assert_allclose(self.result.bse[2], self.true["se_ma_opg"], atol=1e-7)

    def test_bse_approx(self):
        # complex step
        bse = self.result._cov_params_approx(approx_complex_step=True).diagonal() ** 0.5
        assert_allclose(bse[1], self.true["se_ar_oim"], atol=1e-7)
        assert_allclose(bse[2], self.true["se_ma_oim"], atol=1e-7)

        # The below tests pass irregularly; they give a sense of the precision
        # available with finite differencing
        # finite difference, non-centered
        # with warnings.catch_warnings():
        #     warnings.simplefilter("ignore")
        #     bse = self.result._cov_params_approx(
        #         approx_complex_step=False).diagonal()**0.5
        #     assert_allclose(bse[1], self.true['se_ar_oim'], atol=1e-2)
        #     assert_allclose(bse[2], self.true['se_ma_oim'], atol=1e-1)

        #     # finite difference, centered
        #     cpa = self.result._cov_params_approx(
        #         approx_complex_step=False, approx_centered=True)
        #     bse = cpa.diagonal()**0.5
        #     assert_allclose(bse[1], self.true['se_ar_oim'], atol=1e-3)
        #     assert_allclose(bse[2], self.true['se_ma_oim'], atol=1e-3)

    def test_bse_oim(self):
        # OIM covariance type
        oim_bse = self.result.cov_params_oim.diagonal() ** 0.5
        assert_allclose(oim_bse[1], self.true["se_ar_oim"], atol=1e-3)
        assert_allclose(oim_bse[2], self.true["se_ma_oim"], atol=1e-2)

    def test_bse_robust(self):
        robust_oim_bse = self.result.cov_params_robust_oim.diagonal() ** 0.5
        cpra = self.result.cov_params_robust_approx
        robust_approx_bse = cpra.diagonal() ** 0.5
        true_robust_bse = np.r_[self.true["se_ar_robust"], self.true["se_ma_robust"]]

        assert_allclose(robust_oim_bse[1:3], true_robust_bse, atol=1e-2)
        assert_allclose(robust_approx_bse[1:3], true_robust_bse, atol=1e-3)


class TestARIMADiffuse(ARIMA):
    """
    Notes
    -----

    Standard errors are very good for the OPG and quite good for the complex
    step approximation cases.
    """

    @classmethod
    def setup_class(cls, **kwargs):
        kwargs["initialization"] = "approximate_diffuse"
        kwargs["initial_variance"] = results_sarimax.wpi1_diffuse["initial_variance"]
        super().setup_class(results_sarimax.wpi1_diffuse, **kwargs)

    def test_bse(self):
        # test defaults
        assert_equal(self.result.cov_type, "opg")
        assert_equal(self.result._cov_approx_complex_step, True)
        assert_equal(self.result._cov_approx_centered, False)
        # default covariance type (opg)
        assert_allclose(self.result.bse[1], self.true["se_ar_opg"], atol=1e-7)
        assert_allclose(self.result.bse[2], self.true["se_ma_opg"], atol=1e-7)

    def test_bse_approx(self):
        # complex step
        bse = self.result._cov_params_approx(approx_complex_step=True).diagonal() ** 0.5
        assert_allclose(bse[1], self.true["se_ar_oim"], atol=1e-4)
        assert_allclose(bse[2], self.true["se_ma_oim"], atol=1e-4)

        # The below tests do not pass
        # with warnings.catch_warnings():
        #     warnings.simplefilter("ignore")

        #     # finite difference, non-centered : failure
        #     bse = self.result._cov_params_approx(
        #         approx_complex_step=False).diagonal()**0.5
        #     assert_allclose(bse[1], self.true['se_ar_oim'], atol=1e-4)
        #     assert_allclose(bse[2], self.true['se_ma_oim'], atol=1e-4)

        #     # finite difference, centered : failure
        #     cpa = self.result._cov_params_approx(
        #         approx_complex_step=False, approx_centered=True)
        #     bse = cpa.diagonal()**0.5
        #     assert_allclose(bse[1], self.true['se_ar_oim'], atol=1e-4)
        #     assert_allclose(bse[2], self.true['se_ma_oim'], atol=1e-4)

    def test_bse_oim(self):
        # OIM covariance type
        bse = self.result._cov_params_oim().diagonal() ** 0.5
        assert_allclose(bse[1], self.true["se_ar_oim"], atol=1e-2)
        assert_allclose(bse[2], self.true["se_ma_oim"], atol=1e-1)


class AdditiveSeasonal(SARIMAXStataTests):
    """
    ARIMA model with additive seasonal effects

    Stata arima documentation, Example 2
    """

    @classmethod
    def setup_class(cls, true, *args, **kwargs):
        cls.true = true
        endog = np.log(true["data"])

        kwargs.setdefault("simple_differencing", True)
        kwargs.setdefault("hamilton_representation", True)

        cls.model = sarimax.SARIMAX(
            endog, *args, order=(1, 1, (1, 0, 0, 1)), trend="c", **kwargs
        )

        # Stata estimates the mean of the process, whereas SARIMAX estimates
        # the intercept of the process. Get the intercept.
        intercept = (1 - true["params_ar"][0]) * true["params_mean"][0]
        params = np.r_[
            intercept, true["params_ar"], true["params_ma"], true["params_variance"]
        ]

        cls.result = cls.model.filter(params)

    def test_mle(self):
        result = self.model.fit(disp=-1)
        assert_allclose(result.params, self.result.params, atol=1e-3)


class TestAdditiveSeasonal(AdditiveSeasonal):
    """
    Notes
    -----

    Standard errors are very good for the OPG and quite good for the complex
    step approximation cases.
    """

    @classmethod
    def setup_class(cls):
        super().setup_class(results_sarimax.wpi1_seasonal)

    def test_bse(self):
        # test defaults
        assert_equal(self.result.cov_type, "opg")
        assert_equal(self.result._cov_approx_complex_step, True)
        assert_equal(self.result._cov_approx_centered, False)
        # default covariance type (opg)
        assert_allclose(self.result.bse[1], self.true["se_ar_opg"], atol=1e-6)
        assert_allclose(self.result.bse[2:4], self.true["se_ma_opg"], atol=1e-5)

    def test_bse_approx(self):
        # complex step
        bse = self.result._cov_params_approx(approx_complex_step=True).diagonal() ** 0.5
        assert_allclose(bse[1], self.true["se_ar_oim"], atol=1e-4)
        assert_allclose(bse[2:4], self.true["se_ma_oim"], atol=1e-4)

        # The below tests pass irregularly; they give a sense of the precision
        # available with finite differencing
        # with warnings.catch_warnings():
        #     warnings.simplefilter("ignore")

        #     # finite difference, non-centered
        #     bse = self.result._cov_params_approx(
        #         approx_complex_step=False).diagonal()**0.5
        #     assert_allclose(bse[1], self.true['se_ar_oim'], atol=1e-2)
        #     assert_allclose(bse[2:4], self.true['se_ma_oim'], atol=1e-2)

        #     # finite difference, centered
        #     cpa = self.result._cov_params_approx(
        #         approx_complex_step=False, approx_centered=True)
        #     bse = cpa.diagonal()**0.5
        #     assert_allclose(bse[1], self.true['se_ar_oim'], atol=1e-3)
        #     assert_allclose(bse[2:4], self.true['se_ma_oim'], atol=1e-3)

    def test_bse_oim(self):
        # OIM covariance type
        bse = self.result._cov_params_oim().diagonal() ** 0.5
        assert_allclose(bse[1], self.true["se_ar_oim"], atol=1e-2)
        assert_allclose(bse[2:4], self.true["se_ma_oim"], atol=1e-1)


class Airline(SARIMAXStataTests):
    """
    Multiplicative SARIMA model: "Airline" model

    Stata arima documentation, Example 3
    """

    @classmethod
    def setup_class(cls, true, *args, **kwargs):
        cls.true = true
        endog = np.log(true["data"])

        kwargs.setdefault("simple_differencing", True)
        kwargs.setdefault("hamilton_representation", True)

        cls.model = sarimax.SARIMAX(
            endog,
            *args,
            order=(0, 1, 1),
            seasonal_order=(0, 1, 1, 12),
            trend="n",
            **kwargs,
        )

        params = np.r_[
            true["params_ma"], true["params_seasonal_ma"], true["params_variance"]
        ]

        cls.result = cls.model.filter(params)

    def test_mle(self):
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")

            result = self.model.fit(disp=-1)
            assert_allclose(result.params, self.result.params, atol=1e-4)


class TestAirlineHamilton(Airline):
    """
    Notes
    -----

    Standard errors are very good for the OPG and complex step approximation
    cases.
    """

    @classmethod
    def setup_class(cls):
        super().setup_class(results_sarimax.air2_stationary)

    def test_bse(self):
        # test defaults
        assert_equal(self.result.cov_type, "opg")
        assert_equal(self.result._cov_approx_complex_step, True)
        assert_equal(self.result._cov_approx_centered, False)
        # default covariance type (opg)
        assert_allclose(self.result.bse[0], self.true["se_ma_opg"], atol=1e-6)
        assert_allclose(self.result.bse[1], self.true["se_seasonal_ma_opg"], atol=1e-6)

    def test_bse_approx(self):
        # complex step
        bse = self.result._cov_params_approx(approx_complex_step=True).diagonal() ** 0.5
        assert_allclose(bse[0], self.true["se_ma_oim"], atol=1e-6)
        assert_allclose(bse[1], self.true["se_seasonal_ma_oim"], atol=1e-6)

        # The below tests pass irregularly; they give a sense of the precision
        # available with finite differencing
        # with warnings.catch_warnings():
        #     warnings.simplefilter("ignore")

        #     # finite difference, non-centered
        #     bse = self.result._cov_params_approx(
        #         approx_complex_step=False).diagonal()**0.5
        #     assert_allclose(bse[0], self.true['se_ma_oim'], atol=1e-2)
        #     assert_allclose(bse[1], self.true['se_seasonal_ma_oim'],
        #                     atol=1e-2)

        #     # finite difference, centered
        #     cpa = self.result._cov_params_approx(
        #         approx_complex_step=False, approx_centered=True)
        #     bse = cpa.diagonal()**0.5
        #     assert_allclose(bse[0], self.true['se_ma_oim'], atol=1e-4)
        #     assert_allclose(bse[1], self.true['se_seasonal_ma_oim'],
        #                     atol=1e-4)

    def test_bse_oim(self):
        # OIM covariance type
        oim_bse = self.result.cov_params_oim.diagonal() ** 0.5
        assert_allclose(oim_bse[0], self.true["se_ma_oim"], atol=1e-1)
        assert_allclose(oim_bse[1], self.true["se_seasonal_ma_oim"], atol=1e-1)


class TestAirlineHarvey(Airline):
    """
    Notes
    -----

    Standard errors are very good for the OPG and complex step approximation
    cases.
    """

    @classmethod
    def setup_class(cls):
        super().setup_class(
            results_sarimax.air2_stationary, hamilton_representation=False
        )

    def test_bse(self):
        # test defaults
        assert_equal(self.result.cov_type, "opg")
        assert_equal(self.result._cov_approx_complex_step, True)
        assert_equal(self.result._cov_approx_centered, False)
        # default covariance type (opg)
        assert_allclose(self.result.bse[0], self.true["se_ma_opg"], atol=1e-6)
        assert_allclose(self.result.bse[1], self.true["se_seasonal_ma_opg"], atol=1e-6)

    def test_bse_approx(self):
        # complex step
        bse = self.result._cov_params_approx(approx_complex_step=True).diagonal() ** 0.5
        assert_allclose(bse[0], self.true["se_ma_oim"], atol=1e-6)
        assert_allclose(bse[1], self.true["se_seasonal_ma_oim"], atol=1e-6)

        # The below tests pass irregularly; they give a sense of the precision
        # available with finite differencing
        # with warnings.catch_warnings():
        #     warnings.simplefilter("ignore")

        #     # finite difference, non-centered
        #     bse = self.result._cov_params_approx(
        #         approx_complex_step=False).diagonal()**0.5
        #     assert_allclose(bse[0], self.true['se_ma_oim'], atol=1e-2)
        #     assert_allclose(bse[1], self.true['se_seasonal_ma_oim'],
        #                     atol=1e-2)

        #     # finite difference, centered
        #     cpa = self.result._cov_params_approx(
        #         approx_complex_step=False, approx_centered=True)
        #     bse = cpa.diagonal()**0.5
        #     assert_allclose(bse[0], self.true['se_ma_oim'], atol=1e-4)
        #     assert_allclose(bse[1], self.true['se_seasonal_ma_oim'],
        #                     atol=1e-4)

    def test_bse_oim(self):
        # OIM covariance type
        oim_bse = self.result.cov_params_oim.diagonal() ** 0.5
        assert_allclose(oim_bse[0], self.true["se_ma_oim"], atol=1e-1)
        assert_allclose(oim_bse[1], self.true["se_seasonal_ma_oim"], atol=1e-1)


class TestAirlineStateDifferencing(Airline):
    """
    Notes
    -----

    Standard errors are very good for the OPG and quite good for the complex
    step approximation cases.
    """

    @classmethod
    def setup_class(cls):
        super().setup_class(
            results_sarimax.air2_stationary,
            simple_differencing=False,
            hamilton_representation=False,
        )

    def test_bic(self):
        # Due to diffuse component of the state (which technically changes the
        # BIC calculation - see Durbin and Koopman section 7.4), this is the
        # best we can do for BIC
        assert_almost_equal(self.result.bic, self.true["bic"], 0)

    def test_mle(self):
        result = self.model.fit(method="nm", maxiter=1000, disp=0)
        assert_allclose(result.params, self.result.params, atol=1e-3)

    def test_bse(self):
        # test defaults
        assert_equal(self.result.cov_type, "opg")
        assert_equal(self.result._cov_approx_complex_step, True)
        assert_equal(self.result._cov_approx_centered, False)
        # default covariance type (opg)
        assert_allclose(self.result.bse[0], self.true["se_ma_opg"], atol=1e-6)
        assert_allclose(self.result.bse[1], self.true["se_seasonal_ma_opg"], atol=1e-6)

    def test_bse_approx(self):
        # complex step
        bse = self.result._cov_params_approx(approx_complex_step=True).diagonal() ** 0.5
        assert_allclose(bse[0], self.true["se_ma_oim"], atol=1e-4)
        assert_allclose(bse[1], self.true["se_seasonal_ma_oim"], atol=1e-4)

        # The below tests do not pass
        # with warnings.catch_warnings():
        #     warnings.simplefilter("ignore")

        #     # finite difference, non-centered : failure with NaNs
        #     bse = self.result._cov_params_approx(
        #         approx_complex_step=False).diagonal()**0.5
        #     assert_allclose(bse[0], self.true['se_ma_oim'], atol=1e-2)
        #     assert_allclose(bse[1], self.true['se_seasonal_ma_oim'],
        #                     atol=1e-2)

        #     # finite difference, centered : failure with NaNs
        #     cpa = self.result._cov_params_approx(
        #         approx_complex_step=False, approx_centered=True)
        #     bse = cpa.diagonal()**0.5
        #     assert_allclose(bse[0], self.true['se_ma_oim'], atol=1e-4)
        #     assert_allclose(bse[1], self.true['se_seasonal_ma_oim'],
        #                     atol=1e-4)

    def test_bse_oim(self):
        # OIM covariance type
        oim_bse = self.result.cov_params_oim.diagonal() ** 0.5
        assert_allclose(oim_bse[0], self.true["se_ma_oim"], atol=1e-1)
        assert_allclose(oim_bse[1], self.true["se_seasonal_ma_oim"], atol=1e-1)


class Friedman(SARIMAXStataTests):
    """
    ARMAX model: Friedman quantity theory of money

    Stata arima documentation, Example 4
    """

    @classmethod
    def setup_class(cls, true, exog=None, *args, **kwargs):
        cls.true = true
        endog = np.r_[true["data"]["consump"]]
        if exog is None:
            exog = add_constant(true["data"]["m2"])

        kwargs.setdefault("simple_differencing", True)
        kwargs.setdefault("hamilton_representation", True)

        cls.model = sarimax.SARIMAX(endog,  *args, exog=exog, order=(1, 0, 1), **kwargs)

        params = np.r_[
            true["params_exog"],
            true["params_ar"],
            true["params_ma"],
            true["params_variance"],
        ]

        cls.result = cls.model.filter(params)


class TestFriedmanMLERegression(Friedman):
    """
    Notes
    -----

    Standard errors are very good for the OPG and complex step approximation
    cases.
    """

    @classmethod
    def setup_class(cls):
        super().setup_class(results_sarimax.friedman2_mle)

    def test_mle(self):
        result = self.model.fit(disp=-1)
        # Use ratio to make atol more meaningful parameter scale differs
        ratio = result.params / self.result.params
        assert_allclose(ratio, np.ones(5), atol=1e-2, rtol=1e-3)

    def test_bse(self):
        # test defaults
        assert_equal(self.result.cov_type, "opg")
        assert_equal(self.result._cov_approx_complex_step, True)
        assert_equal(self.result._cov_approx_centered, False)
        # default covariance type (opg)
        assert_allclose(self.result.bse[0:2], self.true["se_exog_opg"], atol=1e-4)
        assert_allclose(self.result.bse[2], self.true["se_ar_opg"], atol=1e-6)
        assert_allclose(self.result.bse[3], self.true["se_ma_opg"], atol=1e-6)

    def test_bse_approx(self):
        # complex step
        bse = self.result._cov_params_approx(approx_complex_step=True).diagonal() ** 0.5
        assert_allclose(bse[0:2], self.true["se_exog_oim"], atol=1e-4)
        assert_allclose(bse[2], self.true["se_ar_oim"], atol=1e-6)
        assert_allclose(bse[3], self.true["se_ma_oim"], atol=1e-6)

        # The below tests pass irregularly; they give a sense of the precision
        # available with finite differencing
        # with warnings.catch_warnings():
        #     warnings.simplefilter("ignore")

        #     # finite difference, non-centered
        #     bse = self.result._cov_params_approx(
        #         approx_complex_step=False).diagonal()**0.5
        #     assert_allclose(bse[0], self.true['se_exog_oim'][0], rtol=1)
        #     assert_allclose(bse[1], self.true['se_exog_oim'][1], atol=1e-2)
        #     assert_allclose(bse[2], self.true['se_ar_oim'], atol=1e-2)
        #     assert_allclose(bse[3], self.true['se_ma_oim'], atol=1e-2)

        #     # finite difference, centered
        #     cpa = self.result._cov_params_approx(
        #         approx_complex_step=False, approx_centered=True)
        #     bse = cpa.diagonal()**0.5
        #     assert_allclose(bse[0], self.true['se_exog_oim'][0], rtol=1)
        #     assert_allclose(bse[1], self.true['se_exog_oim'][1], atol=1e-2)
        #     assert_allclose(bse[2], self.true['se_ar_oim'], atol=1e-2)
        #     assert_allclose(bse[3], self.true['se_ma_oim'], atol=1e-2)

    def test_bse_oim(self):
        # OIM covariance type
        bse = self.result.cov_params_oim.diagonal() ** 0.5
        assert_allclose(bse[0], self.true["se_exog_oim"][0], rtol=1)
        assert_allclose(bse[1], self.true["se_exog_oim"][1], atol=1e-2)
        assert_allclose(bse[2], self.true["se_ar_oim"], atol=1e-2)
        assert_allclose(bse[3], self.true["se_ma_oim"], atol=1e-2)


class TestFriedmanStateRegression(Friedman):
    """
    Notes
    -----

    MLE is not very close and standard errors are not very close for any set of
    parameters.

    This is likely because we're comparing against the model where the
    regression coefficients are also estimated by MLE. So this test should be
    considered just a very basic "sanity" test.
    """

    @classmethod
    def setup_class(cls):
        # Remove the regression coefficients from the parameters, since they
        # will be estimated as part of the state vector
        true = dict(results_sarimax.friedman2_mle)
        exog = add_constant(true["data"]["m2"]) / 10.0

        true["mle_params_exog"] = true["params_exog"][:]
        true["mle_se_exog"] = true["se_exog_opg"][:]

        true["params_exog"] = []
        true["se_exog"] = []

        super().setup_class(true, exog=exog, mle_regression=False)

        cls.true_params = np.r_[
            true["params_exog"],
            true["params_ar"],
            true["params_ma"],
            true["params_variance"],
        ]

        cls.result = cls.model.filter(cls.true_params)

    def test_mle(self):
        result = self.model.fit(disp=-1)
        assert_allclose(result.params, self.result.params, atol=1e-1, rtol=2e-1)

    def test_regression_parameters(self):
        # The regression effects are integrated into the state vector as
        # the last two states (thus the index [-2:]). The filtered
        # estimates of the state vector produced by the Kalman filter and
        # stored in `filtered_state` for these state elements give the
        # recursive least squares estimates of the regression coefficients
        # at each time period. To get the estimates conditional on the
        # entire dataset, use the filtered states from the last time
        # period (thus the index [-1]).
        assert_almost_equal(
            self.result.filter_results.filtered_state[-2:, -1] / 10.0,
            self.true["mle_params_exog"],
            1,
        )

    # Loglikelihood (and so aic, bic) is slightly different when states are
    # integrated into the state vector
    def test_loglike(self):
        pass

    def test_aic(self):
        pass

    def test_bic(self):
        pass

    def test_bse(self):
        # test defaults
        assert_equal(self.result.cov_type, "opg")
        assert_equal(self.result._cov_approx_complex_step, True)
        assert_equal(self.result._cov_approx_centered, False)
        # default covariance type (opg)
        assert_allclose(self.result.bse[0], self.true["se_ar_opg"], atol=1e-2)
        assert_allclose(self.result.bse[1], self.true["se_ma_opg"], atol=1e-2)

    def test_bse_approx(self):
        # complex step
        bse = self.result._cov_params_approx(approx_complex_step=True).diagonal() ** 0.5
        assert_allclose(bse[0], self.true["se_ar_oim"], atol=1e-1)
        assert_allclose(bse[1], self.true["se_ma_oim"], atol=1e-1)

        # The below tests do not pass
        # with warnings.catch_warnings():
        #     warnings.simplefilter("ignore")

        #     # finite difference, non-centered :
        #     #  failure (catastrophic cancellation)
        #     bse = self.result._cov_params_approx(
        #         approx_complex_step=False).diagonal()**0.5
        #     assert_allclose(bse[0], self.true['se_ar_oim'], atol=1e-3)
        #     assert_allclose(bse[1], self.true['se_ma_oim'], atol=1e-2)

        #     # finite difference, centered : failure (nan)
        #     cpa = self.result._cov_params_approx(
        #         approx_complex_step=False, approx_centered=True)
        #     bse = cpa.diagonal()**0.5
        #     assert_allclose(bse[0], self.true['se_ar_oim'], atol=1e-3)
        #     assert_allclose(bse[1], self.true['se_ma_oim'], atol=1e-3)

    def test_bse_oim(self):
        # OIM covariance type
        bse = self.result._cov_params_oim().diagonal() ** 0.5
        assert_allclose(bse[0], self.true["se_ar_oim"], atol=1e-1)
        assert_allclose(bse[1], self.true["se_ma_oim"], atol=1e-1)


class TestFriedmanPredict(Friedman):
    """
    ARMAX model: Friedman quantity theory of money, prediction

    Stata arima postestimation documentation, Example 1 - Dynamic forecasts

    This follows the given Stata example, although it is not truly forecasting
    because it compares using the actual data (which is available in the
    example but just not used in the parameter MLE estimation) against dynamic
    prediction of that data. Here `test_predict` matches the first case, and
    `test_dynamic_predict` matches the second.
    """

    @classmethod
    def setup_class(cls):
        super().setup_class(results_sarimax.friedman2_predict)

    # loglike, aic, bic are not the point of this test (they could pass, but we
    # would have to modify the data so that they were calculated to
    # exclude the last 15 observations)
    def test_loglike(self):
        pass

    def test_aic(self):
        pass

    def test_bic(self):
        pass

    def test_predict(self):
        assert_almost_equal(self.result.predict(), self.true["predict"], 3)

    def test_dynamic_predict(self):
        dynamic = len(self.true["data"]["consump"]) - 15 - 1
        assert_almost_equal(
            self.result.predict(dynamic=dynamic), self.true["dynamic_predict"], 3
        )


class TestFriedmanForecast(Friedman):
    """
    ARMAX model: Friedman quantity theory of money, forecasts

    Variation on:
    Stata arima postestimation documentation, Example 1 - Dynamic forecasts

    This is a variation of the Stata example, in which the endogenous data is
    actually made to be missing so that the predict command must forecast.

    As another unit test, we also compare against the case in State when
    predict is used against missing data (so forecasting) with the dynamic
    option also included. Note, however, that forecasting in State space models
    amounts to running the Kalman filter against missing datapoints, so it is
    not clear whether "dynamic" forecasting (where instead of missing
    datapoints for lags, we plug in previous forecasted endog values) is
    meaningful.
    """

    @classmethod
    def setup_class(cls):
        true = dict(results_sarimax.friedman2_predict)

        true["forecast_data"] = {
            "consump": true["data"]["consump"][-15:],
