"""
Test for ratio of Poisson intensities in two independent samples

Author: Josef Perktold
License: BSD-3

"""

import warnings

import numpy as np
from scipy import optimize, stats

from statsmodels.stats._inference_tools import _mover_confint
from statsmodels.stats.base import HolderTuple
from statsmodels.stats.weightstats import _zstat_generic2

# shorthand
norm = stats.norm


method_names_poisson_1samp = {
    "test": [
        "wald",
        "score",
        "exact-c",
        "midp-c",
        "waldccv",
        "sqrt-a",
        "sqrt-v",
        "sqrt",
    ],
    "confint": [
        "wald",
        "score",
        "exact-c",
        "midp-c",
        "jeff",
        "waldccv",
        "sqrt-a",
        "sqrt-v",
        "sqrt",
        "sqrt-cent",
        "sqrt-centcc",
    ],
}


def test_poisson(
    count, nobs, value, method=None, alternative="two-sided", dispersion=1
):
    """Test for one sample poisson mean or rate

    Parameters
    ----------
    count : array_like
        Observed count, number of events.
    nobs : arrat_like
        Currently this is total exposure time of the count variable.
        This will likely change.
    value : float, array_like
        This is the value of poisson rate under the null hypothesis.
    method : str
        Method to use for confidence interval.
        This is required, there is currently no default method.
        See Notes for available methods.
    alternative : {'two-sided', 'smaller', 'larger'}
        alternative hypothesis, which can be two-sided or either one of the
        one-sided tests.
    dispersion : float
        Dispersion scale coefficient for Poisson QMLE. Default is that the
        data follows a Poisson distribution. Dispersion different from 1
        correspond to excess-dispersion in Poisson quasi-likelihood (GLM).
        Dispersion coeffficient different from one is currently only used in
        wald and score method.

    Returns
    -------
    HolderTuple instance with test statistic, pvalue and other attributes.

    Notes
    -----
    The implementatio of the hypothesis test is mainly based on the references
    for the confidence interval, see confint_poisson.

    Available methods are:

    - "score" : based on score test, uses variance under null value
    - "wald" : based on wald test, uses variance base on estimated rate.
    - "waldccv" : based on wald test with 0.5 count added to variance
      computation. This does not use continuity correction for the center of
      the confidence interval.
    - "exact-c" central confidence interval based on gamma distribution
    - "midp-c" : based on midp correction of central exact confidence interval.
      this uses numerical inversion of the test function. not vectorized.
    - "sqrt" : based on square root transformed counts
    - "sqrt-a" based on Anscombe square root transformation of counts + 3/8.

    See Also
    --------
    confint_poisson

    """

    n = nobs  # short hand
    rate = count / n

    if method is None:
        msg = "method needs to be specified, currently no default method"
        raise ValueError(msg)

    if dispersion != 1:
        if method not in ["wald", "waldcc", "score"]:
            msg = "excess dispersion only supported in wald and score methods"
            raise ValueError(msg)

    dist = "normal"

    if method == "wald":
        std = np.sqrt(dispersion * rate / n)
        statistic = (rate - value) / std

    elif method == "waldccv":
        # WCC in Barker 2002
        # add 0.5 event, not 0.5 event rate as in waldcc
        # std = np.sqrt((rate + 0.5 / n) / n)
        # statistic = (rate + 0.5 / n - value) / std
        std = np.sqrt(dispersion * (rate + 0.5 / n) / n)
        statistic = (rate - value) / std

    elif method == "score":
        std = np.sqrt(dispersion * value / n)
        statistic = (rate - value) / std
        pvalue = stats.norm.sf(statistic)

    elif method.startswith(("exact-c", "midp-c")):
        pv1 = stats.poisson.cdf(count, n * value)
        pv2 = stats.poisson.sf(count - 1, n * value)
        if method.startswith("midp-c"):
            pv1 = pv1 - 0.5 * stats.poisson.pmf(count, n * value)
            pv2 = pv2 - 0.5 * stats.poisson.pmf(count, n * value)
        if alternative == "two-sided":
            pvalue = 2 * np.minimum(pv1, pv2)
        elif alternative == "larger":
            pvalue = pv2
        elif alternative == "smaller":
            pvalue = pv1
        else:
            msg = 'alternative should be "two-sided", "larger" or "smaller"'
            raise ValueError(msg)

        statistic = np.nan
        dist = "Poisson"

    elif method == "sqrt":
        std = 0.5
        statistic = (np.sqrt(count) - np.sqrt(n * value)) / std

    elif method == "sqrt-a":
        # anscombe, based on Swift 2009 (with transformation to rate)
        std = 0.5
        statistic = (np.sqrt(count + 3 / 8) - np.sqrt(n * value + 3 / 8)) / std

    elif method == "sqrt-v":
        # vandenbroucke, based on Swift 2009 (with transformation to rate)
        std = 0.5
        crit = stats.norm.isf(0.025)
        statistic = (
            np.sqrt(count + (crit**2 + 2) / 12)
            -
            # np.sqrt(n * value + (crit**2 + 2) / 12)) / std
            np.sqrt(n * value)
        ) / std

    else:
        raise ValueError("unknown method %s" % method)

    if dist == "normal":
        statistic, pvalue = _zstat_generic2(statistic, 1, alternative)

    res = HolderTuple(
        statistic=statistic,
        pvalue=np.clip(pvalue, 0, 1),
        distribution=dist,
        method=method,
        alternative=alternative,
        rate=rate,
        nobs=n,
    )
    return res


def confint_poisson(count, exposure, method=None, alpha=0.05, alternative="two-sided"):
    """Confidence interval for a Poisson mean or rate

    The function is vectorized for all methods except "midp-c", which uses
    an iterative method to invert the hypothesis test function.

    All current methods are central, that is the probability of each tail is
    smaller or equal to alpha / 2.

    Parameters
    ----------
    count : array_like
        Observed count, number of events.
    exposure : arrat_like
        Currently this is total exposure time of the count variable.
        This will likely change.
    method : str
        Method to use for confidence interval
        This is required, there is currently no default method
    alpha : float in (0, 1)
        Significance level, nominal coverage of the confidence interval is
        1 - alpha.
    alternative : {"two-sider", "larger", "smaller")
        default: "two-sided"
        Specifies whether to calculate a two-sided or one-sided confidence
        interval.

    Returns
    -------
    tuple (low, upp) : confidence limits.
        When alternative is not "two-sided", lower or upper bound is set to
        0 or inf respectively.

    Notes
    -----
    Methods are mainly based on Barker (2002) [1]_ and Swift (2009) [3]_.

    Available methods are:

    - "exact-c" central confidence interval based on gamma distribution
    - "score" : based on score test, uses variance under null value
    - "wald" : based on wald test, uses variance base on estimated rate.
    - "waldccv" : based on wald test with 0.5 count added to variance
      computation. This does not use continuity correction for the center of
      the confidence interval.
    - "midp-c" : based on midp correction of central exact confidence interval.
      this uses numerical inversion of the test function. not vectorized.
    - "jeffreys" : based on Jeffreys' prior. computed using gamma distribution
    - "sqrt" : based on square root transformed counts
    - "sqrt-a" based on Anscombe square root transformation of counts + 3/8.
    - "sqrt-centcc" will likely be dropped. anscombe with continuity corrected
      center.
      (Similar to R survival cipoisson, but without the 3/8 right shift of
      the confidence interval).

    sqrt-cent is the same as sqrt-a, using a different computation, will be
    deleted.

    sqrt-v is a corrected square root method attributed to vandenbrouke, which
    might also be deleted.

    Todo:

    - missing dispersion,
    - maybe split nobs and exposure (? needed in NB). Exposure could be used
      to standardize rate.
    - modified wald, switch method if count=0.

    See Also
    --------
    test_poisson

    References
    ----------
    .. [1] Barker, Lawrence. 2002. “A Comparison of Nine Confidence Intervals
       for a Poisson Parameter When the Expected Number of Events Is ≤ 5.”
       The American Statistician 56 (2): 85-89.
       https://doi.org/10.1198/000313002317572736.
    .. [2] Patil, VV, and HV Kulkarni. 2012. “Comparison of Confidence
       Intervals for the Poisson Mean: Some New Aspects.”
       REVSTAT-Statistical Journal 10(2): 211-27.
    .. [3] Swift, Michael Bruce. 2009. “Comparison of Confidence Intervals for
       a Poisson Mean - Further Considerations.” Communications in Statistics -
       Theory and Methods 38 (5): 748-59.
       https://doi.org/10.1080/03610920802255856.

    """
    n = exposure  # short hand
    rate = count / exposure

    if alternative == "two-sided":
        alpha = alpha / 2
    elif alternative not in ["larger", "smaller"]:
        raise NotImplementedError(f"alternative {alternative} is not available")

    if method is None:
        msg = "method needs to be specified, currently no default method"
        raise ValueError(msg)

    if method == "wald":
        whalf = stats.norm.isf(alpha) * np.sqrt(rate / n)
        ci = (rate - whalf, rate + whalf)

    elif method == "waldccv":
        # based on WCC in Barker 2002
        # add 0.5 event, not 0.5 event rate as in BARKER waldcc
        whalf = stats.norm.isf(alpha) * np.sqrt((rate + 0.5 / n) / n)
        ci = (rate - whalf, rate + whalf)

    elif method == "score":
        crit = stats.norm.isf(alpha)
        center = count + crit**2 / 2
        whalf = crit * np.sqrt(count + crit**2 / 4)
        ci = ((center - whalf) / n, (center + whalf) / n)

    elif method == "midp-c":
        # note local alpha above is for one tail
        ci = _invert_test_confint(
            count, n, alpha=2 * alpha, method="midp-c", method_start="exact-c"
        )

    elif method == "sqrt":
        # drop, wrong n
        crit = stats.norm.isf(alpha)
        center = rate + crit**2 / (4 * n)
        whalf = crit * np.sqrt(rate / n)
        ci = (center - whalf, center + whalf)

    elif method == "sqrt-cent":
        crit = stats.norm.isf(alpha)
        center = count + crit**2 / 4
        whalf = crit * np.sqrt(count + 3 / 8)
        ci = ((center - whalf) / n, (center + whalf) / n)

    elif method == "sqrt-centcc":
        # drop with cc, does not match cipoisson in R survival
        crit = stats.norm.isf(alpha)
        # avoid sqrt of negative value if count=0
        center_low = np.sqrt(np.maximum(count + 3 / 8 - 0.5, 0))
        center_upp = np.sqrt(count + 3 / 8 + 0.5)
        whalf = crit / 2
        # above is for ci of count
        ci = (
            ((np.maximum(center_low - whalf, 0)) ** 2 - 3 / 8) / n,
            ((center_upp + whalf) ** 2 - 3 / 8) / n,
        )

        # crit = stats.norm.isf(alpha)
        # center = count
        # whalf = crit * np.sqrt((count + 3 / 8 + 0.5))
        # ci = ((center - whalf - 0.5) / n, (center + whalf + 0.5) / n)

    elif method == "sqrt-a":
        # anscombe, based on Swift 2009 (with transformation to rate)
        crit = stats.norm.isf(alpha)
        center = np.sqrt(count + 3 / 8)
        whalf = crit / 2
        # above is for ci of count
        ci = (
            ((np.maximum(center - whalf, 0)) ** 2 - 3 / 8) / n,
            ((center + whalf) ** 2 - 3 / 8) / n,
        )

    elif method == "sqrt-v":
        # vandenbroucke, based on Swift 2009 (with transformation to rate)
        crit = stats.norm.isf(alpha)
        center = np.sqrt(count + (crit**2 + 2) / 12)
        whalf = crit / 2
        # above is for ci of count
        ci = (np.maximum(center - whalf, 0)) ** 2 / n, (center + whalf) ** 2 / n

    elif method in ["gamma", "exact-c"]:
        # garwood exact, gamma
        low = stats.gamma.ppf(alpha, count) / exposure
        upp = stats.gamma.isf(alpha, count + 1) / exposure
        if np.isnan(low).any():
            # case with count = 0
            if np.size(low) == 1:
                low = 0.0
            else:
                low[np.isnan(low)] = 0.0

        ci = (low, upp)

    elif method.startswith("jeff"):
        # jeffreys, gamma
        countc = count + 0.5
        ci = (
            stats.gamma.ppf(alpha, countc) / exposure,
            stats.gamma.isf(alpha, countc) / exposure,
        )

    else:
        raise ValueError("unknown method %s" % method)

    if alternative == "larger":
        ci = (0, ci[1])
    elif alternative == "smaller":
        ci = (ci[0], np.inf)

    ci = (np.maximum(ci[0], 0), ci[1])
    return ci


def tolerance_int_poisson(
    count,
    exposure,
    prob=0.95,
    exposure_new=1.0,
    method=None,
    alpha=0.05,
    alternative="two-sided",
):
    """tolerance interval for a poisson observation

    Parameters
    ----------
    count : array_like
        Observed count, number of events.
    exposure : arrat_like
        Currently this is total exposure time of the count variable.
    prob : float in (0, 1)
        Probability of poisson interval, often called "content".
        With known parameters, each tail would have at most probability
        ``1 - prob / 2`` in the two-sided interval.
    exposure_new : float
        Exposure of the new or predicted observation.
    method : str
        Method to used for confidence interval of the estimate of the
        poisson rate, used in `confint_poisson`.
        This is required, there is currently no default method.
    alpha : float in (0, 1)
        Significance level for the confidence interval of the estimate of the
        Poisson rate. Nominal coverage of the confidence interval is
        1 - alpha.
    alternative : {"two-sider", "larger", "smaller")
        The tolerance interval can be two-sided or one-sided.
        Alternative "larger" provides the upper bound of the confidence
        interval, larger counts are outside the interval.

    Returns
    -------
    tuple (low, upp) of limits of tolerance interval.
        The tolerance interval is a closed interval, that is both ``low`` and
        ``upp`` are in the interval.

    Notes
    -----
    verified against R package tolerance `poistol.int`

    See Also
    --------
    confint_poisson
    confint_quantile_poisson

    References
    ----------
    .. [1] Hahn, Gerald J., and William Q. Meeker. 1991. Statistical Intervals:
       A Guide for Practitioners. 1st ed. Wiley Series in Probability and
       Statistics. Wiley. https://doi.org/10.1002/9780470316771.
    .. [2] Hahn, Gerald J., and Ramesh Chandra. 1981. “Tolerance Intervals for
       Poisson and Binomial Variables.” Journal of Quality Technology 13 (2):
       100-110. https://doi.org/10.1080/00224065.1981.11980998.

    """
    prob_tail = 1 - prob
    alpha_ = alpha
    if alternative != "two-sided":
        # confint_poisson does not have one-sided alternatives
        alpha_ = alpha * 2
    low, upp = confint_poisson(count, exposure, method=method, alpha=alpha_)

    if exposure_new != 1:
        low *= exposure_new
        upp *= exposure_new

    if alternative == "two-sided":
        low_pred = stats.poisson.ppf(prob_tail / 2, low)
        upp_pred = stats.poisson.ppf(1 - prob_tail / 2, upp)
    elif alternative == "larger":
        low_pred = 0
        upp_pred = stats.poisson.ppf(1 - prob_tail, upp)
    elif alternative == "smaller":
        low_pred = stats.poisson.ppf(prob_tail, low)
        upp_pred = np.inf

    # clip -1 of ppf(0)
    low_pred = np.maximum(low_pred, 0)
    return low_pred, upp_pred


def confint_quantile_poisson(
    count,
    exposure,
    prob,
    exposure_new=1.0,
    method=None,
    alpha=0.05,
    alternative="two-sided",
):
    """confidence interval for quantile of poisson random variable

    Parameters
    ----------
    count : array_like
        Observed count, number of events.
    exposure : arrat_like
        Currently this is total exposure time of the count variable.
    prob : float in (0, 1)
        Probability for the quantile, e.g. 0.95 to get the upper 95% quantile.
        With known mean mu, the quantile would be poisson.ppf(prob, mu).
    exposure_new : float
        Exposure of the new or predicted observation.
    method : str
        Method to used for confidence interval of the estimate of the
        poisson rate, used in `confint_poisson`.
        This is required, there is currently no default method.
    alpha : float in (0, 1)
        Significance level for the confidence interval of the estimate of the
        Poisson rate. Nominal coverage of the confidence interval is
        1 - alpha.
    alternative : {"two-sider", "larger", "smaller")
        The tolerance interval can be two-sided or one-sided.
        Alternative "larger" provides the upper bound of the confidence
        interval, larger counts are outside the interval.

    Returns
    -------
    tuple (low, upp) of limits of tolerance interval.
    The confidence interval is a closed interval, that is both ``low`` and
    ``upp`` are in the interval.

    See Also
    --------
    confint_poisson
    tolerance_int_poisson

    References
    ----------
    Hahn, Gerald J, and William Q Meeker. 2010. Statistical Intervals: A Guide
    for Practitioners.
    """
    alpha_ = alpha
    if alternative != "two-sided":
        # confint_poisson does not have one-sided alternatives
        alpha_ = alpha * 2
    low, upp = confint_poisson(count, exposure, method=method, alpha=alpha_)
    if exposure_new != 1:
        low *= exposure_new
        upp *= exposure_new

    if alternative == "two-sided":
        low_pred = stats.poisson.ppf(prob, low)
        upp_pred = stats.poisson.ppf(prob, upp)
    elif alternative == "larger":
        low_pred = 0
        upp_pred = stats.poisson.ppf(prob, upp)
    elif alternative == "smaller":
        low_pred = stats.poisson.ppf(prob, low)
        upp_pred = np.inf

    # clip -1 of ppf(0)
    low_pred = np.maximum(low_pred, 0)
    return low_pred, upp_pred


def _invert_test_confint(
    count, nobs, alpha=0.05, method="midp-c", method_start="exact-c"
):
    """invert hypothesis test to get confidence interval"""

    def func(r):
        v = (test_poisson(count, nobs, value=r, method=method)[1] - alpha) ** 2
        return v

    ci = confint_poisson(count, nobs, method=method_start)
    low = optimize.fmin(func, ci[0], xtol=1e-8, disp=False)
    upp = optimize.fmin(func, ci[1], xtol=1e-8, disp=False)
    assert np.size(low) == 1
    return low[0], upp[0]


def _invert_test_confint_2indep(
    count1,
    exposure1,
    count2,
    exposure2,
    alpha=0.05,
    method="score",
    compare="diff",
    method_start="wald",
):
    """invert hypothesis test to get confidence interval for 2indep"""

    def func(r):
        v = (
            test_poisson_2indep(
                count1,
                exposure1,
                count2,
                exposure2,
                value=r,
                method=method,
                compare=compare,
            )[1]
            - alpha
        ) ** 2
        return v

    ci = confint_poisson_2indep(
        count1, exposure1, count2, exposure2, method=method_start, compare=compare
    )
    low = optimize.fmin(func, ci[0], xtol=1e-8, disp=False)
    upp = optimize.fmin(func, ci[1], xtol=1e-8, disp=False)
    assert np.size(low) == 1
    return low[0], upp[0]


method_names_poisson_2indep = {
    "test": {
        "ratio": [
            "wald",
            "score",
            "score-log",
            "wald-log",
            "exact-cond",
            "cond-midp",
            "sqrt",
            "etest-score",
            "etest-wald",
        ],
        "diff": ["wald", "score", "waldccv", "etest-score", "etest-wald"],
    },
    "confint": {
        "ratio": [
            "waldcc",
            "score",
            "score-log",
            "wald-log",
            "sqrtcc",
            "mover",
        ],
        "diff": ["wald", "score", "waldccv", "mover"],
    },
}


def test_poisson_2indep(
    count1,
    exposure1,
    count2,
    exposure2,
    value=None,
    ratio_null=None,
    method=None,
    compare="ratio",
    alternative="two-sided",
    etest_kwds=None,
):
    """Test for comparing two sample Poisson intensity rates.

    Rates are defined as expected count divided by exposure.

    The Null and alternative hypothesis for the rates, rate1 and rate2, of two
    independent Poisson samples are

    for compare = 'diff'

    - H0: rate1 - rate2 - value = 0
    - H1: rate1 - rate2 - value != 0  if alternative = 'two-sided'
    - H1: rate1 - rate2 - value > 0   if alternative = 'larger'
    - H1: rate1 - rate2 - value < 0   if alternative = 'smaller'

    for compare = 'ratio'

    - H0: rate1 / rate2 - value = 0
    - H1: rate1 / rate2 - value != 0  if alternative = 'two-sided'
    - H1: rate1 / rate2 - value > 0   if alternative = 'larger'
    - H1: rate1 / rate2 - value < 0   if alternative = 'smaller'

    Parameters
    ----------
    count1 : int
        Number of events in first sample, treatment group.
    exposure1 : float
        Total exposure (time * subjects) in first sample.
    count2 : int
        Number of events in second sample, control group.
    exposure2 : float
        Total exposure (time * subjects) in second sample.
    ratio_null: float
        Ratio of the two Poisson rates under the Null hypothesis. Default is 1.
        Deprecated, use ``value`` instead.

        .. deprecated:: 0.14.0

            Use ``value`` instead.

    value : float
        Value of the ratio or difference of 2 independent rates under the null
        hypothesis. Default is equal rates, i.e. 1 for ratio and 0 for diff.

        .. versionadded:: 0.14.0

            Replacement for ``ratio_null``.

    method : string
        Method for the test statistic and the p-value. Defaults to `'score'`.
        see Notes.

        ratio:

        - 'wald': method W1A, wald test, variance based on observed rates
        - 'score': method W2A, score test, variance based on estimate under
          the Null hypothesis
        - 'wald-log': W3A, uses log-ratio, variance based on observed rates
        - 'score-log' W4A, uses log-ratio, variance based on estimate under
          the Null hypothesis
        - 'sqrt': W5A, based on variance stabilizing square root transformation
        - 'exact-cond': exact conditional test based on binomial distribution
           This uses ``binom_test`` which is minlike in the two-sided case.
        - 'cond-midp': midpoint-pvalue of exact conditional test
        - 'etest' or 'etest-score: etest with score test statistic
        - 'etest-wald': etest with wald test statistic

        diff:

        - 'wald',
        - 'waldccv'
        - 'score'
        - 'etest-score' or 'etest: etest with score test statistic
        - 'etest-wald': etest with wald test statistic

    compare : {'diff', 'ratio'}
        Default is "ratio".
        If compare is `ratio`, then the hypothesis test is for the
        rate ratio defined by ratio = rate1 / rate2.
        If compare is `diff`, then the hypothesis test is for
        diff = rate1 - rate2.
    alternative : {"two-sided" (default), "larger", smaller}
        The alternative hypothesis, H1, has to be one of the following

        - 'two-sided': H1: ratio, or diff, of rates is not equal to value
        - 'larger' :   H1: ratio, or diff, of rates is larger than value
        - 'smaller' :  H1: ratio, or diff, of rates is smaller than value
    etest_kwds: dictionary
        Additional optional parameters to be passed to the etest_poisson_2indep
        function, namely y_grid.

    Returns
    -------
    results : instance of HolderTuple class
        The two main attributes are test statistic `statistic` and p-value
        `pvalue`.

    See Also
    --------
    tost_poisson_2indep
    etest_poisson_2indep

    Notes
    -----
    The hypothesis tests for compare="ratio" are based on Gu et al 2018.
    The e-tests are also based on ...

    - 'wald': method W1A, wald test, variance based on separate estimates
    - 'score': method W2A, score test, variance based on estimate under Null
    - 'wald-log': W3A, wald test for log transformed ratio
    - 'score-log' W4A, score test for log transformed ratio
    - 'sqrt': W5A, based on variance stabilizing square root transformation
    - 'exact-cond': exact conditional test based on binomial distribution
    - 'cond-midp': midpoint-pvalue of exact conditional test
    - 'etest': etest with score test statistic
    - 'etest-wald': etest with wald test statistic

    The hypothesis test for compare="diff" are mainly based on Ng et al 2007
    and ...

    - wald
    - score
    - etest-score
    - etest-wald

    Note the etests use the constraint maximum likelihood estimate (cmle) as
    parameters for the underlying Poisson probabilities. The constraint cmle
    parameters are the same as in the score test.
    The E-test in Krishnamoorty and Thomson uses a moment estimator instead of
    the score estimator.

    References
    ----------
    .. [1] Gu, Ng, Tang, Schucany 2008: Testing the Ratio of Two Poisson Rates,
       Biometrical Journal 50 (2008) 2, 2008

    .. [2] Ng, H. K. T., K. Gu, and M. L. Tang. 2007. “A Comparative Study of
       Tests for the Difference of Two Poisson Means.”
       Computational Statistics & Data Analysis 51 (6): 3085-99.
       https://doi.org/10.1016/j.csda.2006.02.004.

    """

    # shortcut names
    y1, n1, y2, n2 = map(np.asarray, [count1, exposure1, count2, exposure2])
    d = n2 / n1
    rate1, rate2 = y1 / n1, y2 / n2
    rates_cmle = None

    if compare == "ratio":
        if method is None:
            # default method
            method = "score"

        if ratio_null is not None:
            warnings.warn(
                "'ratio_null' is deprecated, use 'value' keyword",
                FutureWarning,
                stacklevel=2,
            )
            value = ratio_null
        if ratio_null is None and value is None:
            # default value
            value = ratio_null = 1
        else:
            # for results holder instance, it still contains ratio_null
            ratio_null = value

        r = value
        r_d = r / d  # r1 * n1 / (r2 * n2)

        if method in ["score"]:
            stat = (y1 - y2 * r_d) / np.sqrt((y1 + y2) * r_d)
            dist = "normal"
        elif method in ["wald"]:
            stat = (y1 - y2 * r_d) / np.sqrt(y1 + y2 * r_d**2)
            dist = "normal"
        elif method in ["score-log"]:
            stat = np.log(y1 / y2) - np.log(r_d)
            stat /= np.sqrt((2 + 1 / r_d + r_d) / (y1 + y2))
            dist = "normal"
        elif method in ["wald-log"]:
            stat = (np.log(y1 / y2) - np.log(r_d)) / np.sqrt(1 / y1 + 1 / y2)
            dist = "normal"
        elif method in ["sqrt"]:
            stat = 2 * (np.sqrt(y1 + 3 / 8.0) - np.sqrt((y2 + 3 / 8.0) * r_d))
            stat /= np.sqrt(1 + r_d)
            dist = "normal"
        elif method in ["exact-cond", "cond-midp"]:
            from statsmodels.stats import proportion

            bp = r_d / (1 + r_d)
            y_total = y1 + y2
            stat = np.nan
            # TODO: why y2 in here and not y1, check definition of H1 "larger"
            pvalue = proportion.binom_test(
                y1, y_total, prop=bp, alternative=alternative
            )
            if method in ["cond-midp"]:
                # not inplace in case we still want binom pvalue
                pvalue = pvalue - 0.5 * stats.binom.pmf(y1, y_total, bp)

            dist = "binomial"
        elif method.startswith("etest"):
            if method.endswith("wald"):
                method_etest = "wald"
            else:
                method_etest = "score"
            if etest_kwds is None:
                etest_kwds = {}

            stat, pvalue = etest_poisson_2indep(
                count1,
                exposure1,
                count2,
                exposure2,
                value=value,
                method=method_etest,
                alternative=alternative,
                **etest_kwds,
            )

            dist = "poisson"
        else:
            raise ValueError(f'method "{method}" not recognized')

    elif compare == "diff":
        if value is None:
            value = 0
        if method in ["wald"]:
            stat = (rate1 - rate2 - value) / np.sqrt(rate1 / n1 + rate2 / n2)
            dist = "normal"
            "waldccv"
        elif method in ["waldccv"]:
            stat = rate1 - rate2 - value
            stat /= np.sqrt((count1 + 0.5) / n1**2 + (count2 + 0.5) / n2**2)
            dist = "normal"
        elif method in ["score"]:
            # estimate rates with constraint MLE
            count_pooled = y1 + y2
            rate_pooled = count_pooled / (n1 + n2)
            dt = rate_pooled - value
            r2_cmle = 0.5 * (dt + np.sqrt(dt**2 + 4 * value * y2 / (n1 + n2)))
            r1_cmle = r2_cmle + value

            stat = (rate1 - rate2 - value) / np.sqrt(r1_cmle / n1 + r2_cmle / n2)
            rates_cmle = (r1_cmle, r2_cmle)
            dist = "normal"
        elif method.startswith("etest"):
            if method.endswith("wald"):
                method_etest = "wald"
            else:
                method_etest = "score"
                if method == "etest":
                    method = method + "-score"

            if etest_kwds is None:
                etest_kwds = {}

            stat, pvalue = etest_poisson_2indep(
                count1,
                exposure1,
                count2,
                exposure2,
                value=value,
                method=method_etest,
                compare="diff",
                alternative=alternative,
                **etest_kwds,
            )

            dist = "poisson"
        else:
            raise ValueError(f'method "{method}" not recognized')
    else:
        raise NotImplementedError('"compare" needs to be ratio or diff')

    if dist == "normal":
        stat, pvalue = _zstat_generic2(stat, 1, alternative)

    rates = (rate1, rate2)
    ratio = rate1 / rate2
    diff = rate1 - rate2
    res = HolderTuple(
        statistic=stat,
        pvalue=pvalue,
        distribution=dist,
        compare=compare,
        method=method,
        alternative=alternative,
        rates=rates,
        ratio=ratio,
        diff=diff,
        value=value,
        rates_cmle=rates_cmle,
        ratio_null=ratio_null,
    )
    return res


def _score_diff(y1, n1, y2, n2, value=0, return_cmle=False):
    """score test and cmle for difference of 2 independent poisson rates"""
    count_pooled = y1 + y2
    rate1, rate2 = y1 / n1, y2 / n2
    rate_pooled = count_pooled / (n1 + n2)
    dt = rate_pooled - value
    r2_cmle = 0.5 * (dt + np.sqrt(dt**2 + 4 * value * y2 / (n1 + n2)))
    r1_cmle = r2_cmle + value
    eps = 1e-20  # avoid zero division in stat_func
    v = r1_cmle / n1 + r2_cmle / n2
    stat = (rate1 - rate2 - value) / np.sqrt(v + eps)

    if return_cmle:
        return stat, r1_cmle, r2_cmle
    else:
        return stat


def etest_poisson_2indep(
    count1,
    exposure1,
    count2,
    exposure2,
    ratio_null=None,
    value=None,
    method="score",
    compare="ratio",
    alternative="two-sided",
    ygrid=None,
    y_grid=None,
):
    """
    E-test for ratio of two sample Poisson rates.

    Rates are defined as expected count divided by exposure. The Null and
    alternative hypothesis for the rates, rate1 and rate2, of two independent
    Poisson samples are:

    for compare = 'diff'

    - H0: rate1 - rate2 - value = 0
    - H1: rate1 - rate2 - value != 0  if alternative = 'two-sided'
    - H1: rate1 - rate2 - value > 0   if alternative = 'larger'
    - H1: rate1 - rate2 - value < 0   if alternative = 'smaller'

    for compare = 'ratio'

    - H0: rate1 / rate2 - value = 0
    - H1: rate1 / rate2 - value != 0  if alternative = 'two-sided'
    - H1: rate1 / rate2 - value > 0   if alternative = 'larger'
    - H1: rate1 / rate2 - value < 0   if alternative = 'smaller'

    Parameters
    ----------
    count1 : int
        Number of events in first sample
    exposure1 : float
