from datetime import (
    UTC,
    date,
    datetime,
)

import numpy as np
import pytest

from pandas.errors import Pandas4Warning

import pandas as pd
import pandas._testing as tm

from pandas.tseries.offsets import (
    BDay,
    BMonthEnd,
)


class TestJoin:
    def test_does_not_convert_mixed_integer(self):
        df = pd.DataFrame(
            np.ones((3, 2)), columns=pd.date_range("2020-01-01", periods=2)
        )
        cols = df.columns.join(df.index, how="outer")
        joined = cols.join(df.columns)
        assert cols.dtype == np.dtype("O")
        assert cols.dtype == joined.dtype
        tm.assert_numpy_array_equal(cols.values, joined.values)

    def test_join_self(self, join_type):
        index = pd.date_range("1/1/2000", periods=10)
        joined = index.join(index, how=join_type)
        assert index is joined

    def test_join_with_period_index(self, join_type):
        df = pd.DataFrame(
            np.ones((10, 2)),
            index=pd.date_range("2020-01-01", periods=10),
            columns=pd.period_range("2020-01-01", periods=2),
        )
        s = df.iloc[:5, 0]

        if join_type == "left":
            # GH#63371 a left join keeps the left dtype
            expected = df.columns
        else:
            expected = df.columns.astype("O").join(s.index, how=join_type)
        result = df.columns.join(s.index, how=join_type)
        tm.assert_index_equal(expected, result)

    def test_join_object_index(self):
        rng = pd.date_range("1/1/2000", periods=10)
        idx = pd.Index(["a", "b", "c", "d"])

        result = rng.join(idx, how="outer")
        assert isinstance(result[0], pd.Timestamp)

    def test_join_utc_convert(self, join_type):
        rng = pd.date_range("1/1/2011", periods=100, freq="h", tz="utc")

        left = rng.tz_convert("US/Eastern")
        right = rng.tz_convert("Europe/Berlin")

        result = left.join(left[:-5], how=join_type)
        assert isinstance(result, pd.DatetimeIndex)
        assert result.tz == left.tz

        result = left.join(right[:-5], how=join_type)
        assert isinstance(result, pd.DatetimeIndex)
        assert result.tz is UTC

    def test_datetimeindex_union_join_empty(self, sort, using_infer_string):
        dti = pd.date_range(start="1/1/2001", end="2/1/2001", freq="D")
        empty = pd.Index([])

        result = dti.union(empty, sort=sort)
        if using_infer_string:
            assert isinstance(result, pd.DatetimeIndex)
            tm.assert_index_equal(result, dti)
        else:
            expected = dti.astype("O")
            tm.assert_index_equal(result, expected)

        result = dti.join(empty)
        assert isinstance(result, pd.DatetimeIndex)
        tm.assert_index_equal(result, dti)

    def test_join_nonunique(self):
        idx1 = pd.to_datetime(
            ["2012-11-06 16:00:11.477563", "2012-11-06 16:00:11.477563"]
        )
        idx2 = pd.to_datetime(
            ["2012-11-06 15:11:09.006507", "2012-11-06 15:11:09.006507"]
        )
        rs = idx1.join(idx2, how="outer")
        assert rs.is_monotonic_increasing

    @pytest.mark.parametrize("freq", ["B", "C"])
    def test_outer_join(self, freq):
        # should just behave as union
        start, end = datetime(2009, 1, 1), datetime(2010, 1, 1)
        rng = pd.date_range(start=start, end=end, freq=freq)

        # overlapping
        left = rng[:10]
        right = rng[5:10]

        the_join = left.join(right, how="outer")
        assert isinstance(the_join, pd.DatetimeIndex)

        # non-overlapping, gap in middle
        left = rng[:5]
        right = rng[10:]

        the_join = left.join(right, how="outer")
        assert isinstance(the_join, pd.DatetimeIndex)
        assert the_join.freq is None

        # non-overlapping, no gap
        left = rng[:5]
        right = rng[5:10]

        the_join = left.join(right, how="outer")
        assert isinstance(the_join, pd.DatetimeIndex)

        # overlapping, but different offset
        other = pd.date_range(start, end, freq=BMonthEnd())

        the_join = rng.join(other, how="outer")
        assert isinstance(the_join, pd.DatetimeIndex)
        assert the_join.freq is None

    def test_naive_aware_conflicts(self):
        start, end = datetime(2009, 1, 1), datetime(2010, 1, 1)
        naive = pd.date_range(start, end, freq=BDay(), tz=None)
        aware = pd.date_range(start, end, freq=BDay(), tz="Asia/Hong_Kong")

        msg = "tz-naive.*tz-aware"
        with pytest.raises(TypeError, match=msg):
            naive.join(aware)

        with pytest.raises(TypeError, match=msg):
            aware.join(naive)

    @pytest.mark.parametrize("tz", [None, "US/Pacific"])
    def test_join_preserves_freq(self, tz):
        # GH#32157
        dti = pd.date_range("2016-01-01", periods=10, tz=tz)
        result = dti[:5].join(dti[5:], how="outer")
        assert result.freq == dti.freq
        tm.assert_index_equal(result, dti)

        result = dti[:5].join(dti[6:], how="outer")
        assert result.freq is None
        expected = dti.delete(5)
        tm.assert_index_equal(result, expected)


def test_join_date_objects_with_datetimeindex():
    # GH#62158
    dti = pd.date_range("2016-01-01", periods=3)
    date_idx = pd.Index([date(2016, 1, 1), date(2016, 1, 2), date(2016, 1, 3)])

    msg = "Indexing a DatetimeIndex with a sequence of datetime.date"
    with tm.assert_produces_warning(Pandas4Warning, match=msg):
        result = dti.join(date_idx, how="inner")
    tm.assert_index_equal(result, dti)


def test_join_mixed_date_tzaware_timestamp():
    # GH#68577 the target is not convertible to datetime64, so it is joined as
    #  object instead of raising "Mixed timezones detected"
    ts = pd.Timestamp("2016-01-01", tz="UTC")
    dti = pd.DatetimeIndex([ts])
    target = pd.Index([date(2016, 1, 1), ts], dtype=object)

    result, lindexer, _ = dti.join(target, how="right", return_indexers=True)

    tm.assert_index_equal(result, target)
    # -1 because a date never matches a tz-aware Timestamp
    tm.assert_numpy_array_equal(lindexer, np.array([-1, 0], dtype=np.intp))
