from datetime import (
    datetime,
    timedelta,
    timezone,
)
import subprocess
import sys
import textwrap
import zoneinfo

import dateutil.tz
import numpy as np
import pytest

from pandas._libs.tslibs import (
    conversion,
    timezones,
)
from pandas.compat import is_platform_windows

from pandas import (
    Timestamp,
    date_range,
)
import pandas._testing as tm


@pytest.mark.single_cpu
def test_no_timezone_data():
    # https://github.com/pandas-dev/pandas/pull/63335
    # Test error message when timezone data is not available.
    msg = "'No time zone found with key Europe/Brussels'"
    code = textwrap.dedent(
        f"""\
        import sys, zoneinfo, pandas as pd
        sys.modules['tzdata'] = None
        zoneinfo.reset_tzpath(['/path/to/nowhere'])
        try:
            pd.to_datetime('2012-01-01').tz_localize('Europe/Brussels')
        except zoneinfo.ZoneInfoNotFoundError as err:
            assert str(err) == "{msg}"
        """
    )
    subprocess.check_call([sys.executable, "-c", code])


def test_is_utc(utc_fixture):
    tz = timezones.maybe_get_tz(utc_fixture)
    assert timezones.is_utc(tz)


def test_cache_keys_are_distinct_for_pytz_vs_dateutil():
    pytz = pytest.importorskip("pytz")
    for tz_name in pytz.common_timezones:
        tz_p = timezones.maybe_get_tz(tz_name)
        tz_d = timezones.maybe_get_tz("dateutil/" + tz_name)

    if tz_d is None:
        pytest.skip(tz_name + ": dateutil does not know about this one")

    if not (tz_name == "UTC" and is_platform_windows()):
        # they both end up as tzwin("UTC") on windows
        assert timezones._p_tz_cache_key(tz_p) != timezones._p_tz_cache_key(tz_d)


def test_tzlocal_repr():
    # see gh-13583
    ts = Timestamp("2011-01-01", tz=dateutil.tz.tzlocal())
    assert ts.tz == dateutil.tz.tzlocal()
    assert "tz='tzlocal()')" in repr(ts)


def test_tzlocal_maybe_get_tz():
    # see gh-13583
    tz = timezones.maybe_get_tz("tzlocal()")
    assert tz == dateutil.tz.tzlocal()


def test_tzlocal_offset():
    # see gh-13583
    #
    # Get offset using normal datetime for test.
    ts = Timestamp("2011-01-01", tz=dateutil.tz.tzlocal()).as_unit("s")

    offset = dateutil.tz.tzlocal().utcoffset(datetime(2011, 1, 1))
    offset = offset.total_seconds()

    assert ts._value + offset == Timestamp("2011-01-01").as_unit("s")._value


def test_tzlocal_is_not_utc():
    # even if the machine running the test is localized to UTC
    tz = dateutil.tz.tzlocal()
    assert not timezones.is_utc(tz)

    assert not timezones.tz_compare(tz, dateutil.tz.tzutc())


def test_tz_compare_utc(utc_fixture, utc_fixture2):
    tz = timezones.maybe_get_tz(utc_fixture)
    tz2 = timezones.maybe_get_tz(utc_fixture2)
    assert timezones.tz_compare(tz, tz2)


@pytest.fixture(
    params=[
        ("pytz/US/Eastern", lambda tz, x: tz.localize(x)),
        (dateutil.tz.gettz("US/Eastern"), lambda tz, x: x.replace(tzinfo=tz)),
    ]
)
def infer_setup(request):
    eastern, localize = request.param
    if isinstance(eastern, str) and eastern.startswith("pytz/"):
        pytz = pytest.importorskip("pytz")
        eastern = pytz.timezone(eastern.removeprefix("pytz/"))

    start_naive = datetime(2001, 1, 1)
    end_naive = datetime(2009, 1, 1)

    start = localize(eastern, start_naive)
    end = localize(eastern, end_naive)

    return eastern, localize, start, end, start_naive, end_naive


def test_infer_tz_compat(infer_setup):
    eastern, _, start, end, start_naive, end_naive = infer_setup

    assert (
        timezones.infer_tzinfo(start, end)
        is conversion.localize_pydatetime(start_naive, eastern).tzinfo
    )
    assert (
        timezones.infer_tzinfo(start, None)
        is conversion.localize_pydatetime(start_naive, eastern).tzinfo
    )
    assert (
        timezones.infer_tzinfo(None, end)
        is conversion.localize_pydatetime(end_naive, eastern).tzinfo
    )


def test_infer_tz_utc_localize(infer_setup):
    _, _, start, end, start_naive, end_naive = infer_setup
    utc = timezone.utc

    start = start_naive.astimezone(utc)
    end = end_naive.astimezone(utc)

    assert timezones.infer_tzinfo(start, end) is utc


@pytest.mark.parametrize("ordered", [True, False])
def test_infer_tz_mismatch(infer_setup, ordered):
    eastern, _, _, _, start_naive, end_naive = infer_setup
    msg = "Inputs must both have the same timezone"

    utc = timezone.utc
    start = start_naive.astimezone(utc)
    end = conversion.localize_pydatetime(end_naive, eastern)

    args = (start, end) if ordered else (end, start)

    with pytest.raises(AssertionError, match=msg):
        timezones.infer_tzinfo(*args)


def test_maybe_get_tz_invalid_types():
    with pytest.raises(TypeError, match="<class 'float'>"):
        timezones.maybe_get_tz(44.0)

    with pytest.raises(TypeError, match="<class 'module'>"):
        timezones.maybe_get_tz(pytest)

    msg = "<class 'pandas.Timestamp'>"
    with pytest.raises(TypeError, match=msg):
        timezones.maybe_get_tz(Timestamp("2021-01-01", tz="UTC"))


@pytest.mark.parametrize("tz_name", ["UTC", "GMT", "Etc/GMT+1", "Etc/GMT-5"])
def test_zoneinfo_fixed_offset(tz_name):
    # GH#64363
    zoneinfo = pytest.importorskip("zoneinfo")
    tz = zoneinfo.ZoneInfo(tz_name)
    assert timezones.is_fixed_offset(tz)


def test_zoneinfo_not_fixed_offset_with_historical_transition():
    # GH#64363
    zoneinfo = pytest.importorskip("zoneinfo")
    tz = zoneinfo.ZoneInfo("Africa/Lusaka")
    assert not timezones.is_fixed_offset(tz)


def test_maybe_get_tz_offset_only():
    # see gh-36004

    # timezone.utc
    tz = timezones.maybe_get_tz(timezone.utc)
    assert tz == timezone(timedelta(hours=0, minutes=0))

    # without UTC+- prefix
    tz = timezones.maybe_get_tz("+01:15")
    assert tz == timezone(timedelta(hours=1, minutes=15))

    tz = timezones.maybe_get_tz("-01:15")
    assert tz == timezone(-timedelta(hours=1, minutes=15))

    # with UTC+- prefix
    tz = timezones.maybe_get_tz("UTC+02:45")
    assert tz == timezone(timedelta(hours=2, minutes=45))

    tz = timezones.maybe_get_tz("UTC-02:45")
    assert tz == timezone(-timedelta(hours=2, minutes=45))


def test_zoneinfo_utc_to_local_post_2037():
    # GH#64363 - verify that ZoneInfo DST transitions after 2037
    # (generated from POSIX TZ string rules) produce correct local times.
    tz = zoneinfo.ZoneInfo("US/Pacific")
    utc_times = date_range("2040-07-01", periods=24, freq="h", tz="UTC")
    local = utc_times.tz_convert(tz)

    expected_hours = np.array(
        [
            datetime(2040, 7, 1, hour, tzinfo=timezone.utc).astimezone(tz).hour
            for hour in range(24)
        ],
        dtype=np.int32,
    )
    tm.assert_numpy_array_equal(local.hour.to_numpy(), expected_hours)


@pytest.mark.parametrize("key", ["US/Eastern", "Africa/Lusaka", "Asia/Qyzylorda"])
def test_zoneinfo_utc_to_local_pre_first_transition(key):
    # GH#64363 - verify that ZoneInfo offsets before the first historical
    # transition (LMT era) match what ZoneInfo itself returns. The "before"
    # offset must be utcoff[0] from the TZ data (matching CPython's C
    # implementation), NOT the first non-DST transition offset.
    tz = zoneinfo.ZoneInfo(key)
    ts = Timestamp("1850-01-01", tz="UTC").tz_convert(tz)

    expected = datetime(1850, 1, 1, tzinfo=timezone.utc).astimezone(tz)
    assert ts.minute == expected.minute


def test_normalize_pytz_timezone():
    pytz = pytest.importorskip("pytz")

    from pandas.io._util import _normalize_pytz_timezone

    for tz, expected in [
        (pytz.UTC, timezone.utc),
        (pytz.FixedOffset(90), timezone(timedelta(minutes=90))),
        (pytz.timezone("America/New_York"), zoneinfo.ZoneInfo("America/New_York")),
        (pytz.timezone("Etc/GMT+1"), zoneinfo.ZoneInfo("Etc/GMT+1")),
    ]:
        result = _normalize_pytz_timezone(tz)
        assert result == expected
