import unittest
from datetime import datetime, timedelta
from analogai.foundation.core.value_objects.time import Time
from analogai.foundation.core.value_objects.relation import Relation
from analogai.foundation.common.utils.time_utils import TimeFormatter, Tense


class TestTimeFormatter(unittest.TestCase):
    
    def test_has_time_data(self):
        self.assertTrue(TimeFormatter.has_time_data(Time(year=2024, month=1, day=1)))
        self.assertFalse(TimeFormatter.has_time_data(None))
        self.assertFalse(TimeFormatter.has_time_data(Time()))
        self.assertTrue(TimeFormatter.has_time_data(Time(year=2024)))
        self.assertTrue(TimeFormatter.has_time_data(Time(month=1)))
        self.assertTrue(TimeFormatter.has_time_data(Time(day=1)))
        self.assertTrue(TimeFormatter.has_time_data(Time(hour=10)))
        self.assertTrue(TimeFormatter.has_time_data(Time(minute=30)))
    
    def test_determine_tense_past(self):
        past_time = Time(year=2020, month=1, day=1)
        tense = TimeFormatter.determine_tense(past_time)
        self.assertEqual(tense, Tense.PAST)
    
    def test_determine_tense_future(self):
        future_time = Time(year=2030, month=1, day=1)
        tense = TimeFormatter.determine_tense(future_time)
        self.assertEqual(tense, Tense.FUTURE)
    
    def test_determine_tense_present(self):
        today = datetime.now()
        present_time = Time(year=today.year, month=today.month, day=today.day)
        tense = TimeFormatter.determine_tense(present_time)
        self.assertEqual(tense, Tense.PRESENT)
    
    def test_format_natural_language_today(self):
        today = datetime.now()
        time_obj = Time(year=today.year, month=today.month, day=today.day)
        result = TimeFormatter.format_natural_language(time_obj)
        self.assertEqual(result, "today")
    
    def test_format_natural_language_yesterday(self):
        yesterday = datetime.now() - timedelta(days=1)
        time_obj = Time(year=yesterday.year, month=yesterday.month, day=yesterday.day)
        result = TimeFormatter.format_natural_language(time_obj)
        self.assertEqual(result, "yesterday")
    
    def test_format_natural_language_tomorrow(self):
        tomorrow = datetime.now() + timedelta(days=1)
        time_obj = Time(year=tomorrow.year, month=tomorrow.month, day=tomorrow.day)
        result = TimeFormatter.format_natural_language(time_obj)
        self.assertEqual(result, "tomorrow")
    
    def test_format_natural_language_minutes_ago(self):
        past_time = datetime.now() - timedelta(minutes=30)
        time_obj = Time(year=past_time.year, month=past_time.month, day=past_time.day,
                       hour=past_time.hour, minute=past_time.minute)
        result = TimeFormatter.format_natural_language(time_obj)
        self.assertIn("ago", result)
        self.assertIn("minute", result.lower())
    
    def test_format_natural_language_hours_ago(self):
        now = datetime.now()
        past_time = now - timedelta(hours=3)
        time_obj = Time(year=past_time.year, month=past_time.month, day=past_time.day,
                       hour=past_time.hour, minute=past_time.minute)
        result = TimeFormatter.format_natural_language(time_obj)
        if past_time.date() == now.date():
            self.assertIn("ago", result)
            self.assertIn("hour", result.lower())
        else:
            self.assertIn("yesterday", result.lower())
    
    def test_format_natural_language_days_ago(self):
        past_time = datetime.now() - timedelta(days=5)
        time_obj = Time(year=past_time.year, month=past_time.month, day=past_time.day)
        result = TimeFormatter.format_natural_language(time_obj)
        self.assertIn("ago", result)
        self.assertIn("day", result.lower())
    
    def test_format_natural_language_last_week(self):
        past_time = datetime.now() - timedelta(days=10)
        time_obj = Time(year=past_time.year, month=past_time.month, day=past_time.day)
        result = TimeFormatter.format_natural_language(time_obj)
        self.assertIn("week", result.lower())
    
    def test_format_natural_language_last_month(self):
        past_time = datetime.now() - timedelta(days=45)
        time_obj = Time(year=past_time.year, month=past_time.month, day=past_time.day)
        result = TimeFormatter.format_natural_language(time_obj)
        self.assertIn("month", result.lower())
    
    def test_format_natural_language_last_year(self):
        past_time = datetime.now() - timedelta(days=400)
        time_obj = Time(year=past_time.year, month=past_time.month, day=past_time.day)
        result = TimeFormatter.format_natural_language(time_obj)
        self.assertIn("year", result.lower())

    def test_format_natural_language_more_than_ten_years_returns_year(self):
        reference_time = datetime(2026, 2, 17, 12, 0, 0)
        time_obj = Time(year=1800, month=1, day=1)
        result = TimeFormatter.format_natural_language(time_obj, reference_time=reference_time)
        self.assertEqual(result, "1800")
    
    def test_format_natural_language_future_minutes(self):
        future_time = datetime.now() + timedelta(minutes=30)
        time_obj = Time(year=future_time.year, month=future_time.month, day=future_time.day,
                       hour=future_time.hour, minute=future_time.minute)
        result = TimeFormatter.format_natural_language(time_obj)
        self.assertIn("in", result)
        self.assertIn("minute", result.lower())
    
    def test_format_natural_language_future_hours(self):
        now = datetime.now()
        if now.hour >= 21:
            future_time = now + timedelta(hours=2)
        else:
            future_time = now + timedelta(hours=3)
        time_obj = Time(year=future_time.year, month=future_time.month, day=future_time.day,
                       hour=future_time.hour, minute=future_time.minute)
        result = TimeFormatter.format_natural_language(time_obj)
        if future_time.date() == now.date():
            self.assertIn("in", result)
            self.assertIn("hour", result.lower())
        else:
            self.assertTrue(
                result in ["tomorrow", "in 1 day"] or 
                ("in" in result and ("hour" in result.lower() or "minute" in result.lower()))
            )
    
    def test_format_natural_language_future_days(self):
        future_time = datetime.now() + timedelta(days=5)
        time_obj = Time(year=future_time.year, month=future_time.month, day=future_time.day)
        result = TimeFormatter.format_natural_language(time_obj)
        self.assertIn("in", result)
        self.assertIn("day", result.lower())
    
    def test_format_natural_language_next_week(self):
        future_time = datetime.now() + timedelta(days=10)
        time_obj = Time(year=future_time.year, month=future_time.month, day=future_time.day)
        result = TimeFormatter.format_natural_language(time_obj)
        self.assertIn("week", result.lower())
    
    def test_format_natural_language_next_month(self):
        future_time = datetime.now() + timedelta(days=45)
        time_obj = Time(year=future_time.year, month=future_time.month, day=future_time.day)
        result = TimeFormatter.format_natural_language(time_obj)
        self.assertIn("month", result.lower())
    
    def test_format_natural_language_next_year(self):
        future_time = datetime.now() + timedelta(days=400)
        time_obj = Time(year=future_time.year, month=future_time.month, day=future_time.day)
        result = TimeFormatter.format_natural_language(time_obj)
        self.assertIn("year", result.lower())
    
    def test_format_for_range_this_month(self):
        today = datetime.now()
        time_obj = Time(year=today.year, month=today.month)
        result = TimeFormatter.format_for_range(time_obj)
        self.assertEqual(result, "this month")
    
    def test_format_for_range_last_year(self):
        last_year = datetime.now().year - 1
        time_obj = Time(year=last_year)
        result = TimeFormatter.format_for_range(time_obj)
        self.assertEqual(result, "last year")
    
    def test_format_for_range_next_year(self):
        next_year = datetime.now().year + 1
        time_obj = Time(year=next_year)
        result = TimeFormatter.format_for_range(time_obj)
        self.assertEqual(result, "next year")
    
    def test_format_for_range_last_month(self):
        today = datetime.now()
        last_month = today.month - 1 if today.month > 1 else 12
        last_month_year = today.year if today.month > 1 else today.year - 1
        time_obj = Time(year=last_month_year, month=last_month)
        result = TimeFormatter.format_for_range(time_obj)
        self.assertEqual(result, "last month")
    
    def test_format_for_range_next_month(self):
        today = datetime.now()
        next_month = today.month + 1 if today.month < 12 else 1
        next_month_year = today.year if today.month < 12 else today.year + 1
        time_obj = Time(year=next_month_year, month=next_month)
        result = TimeFormatter.format_for_range(time_obj)
        self.assertEqual(result, "next month")
    
    def test_format_for_range_specific_date(self):
        time_obj = Time(year=2024, month=3, day=15)
        result = TimeFormatter.format_for_range(time_obj)
        self.assertIn("2024", result)
        self.assertIn("March", result)
        self.assertIn("15", result)
    
    def test_format_natural_language_now_when_hour_matches_current_hour_no_minutes(self):
        from zoneinfo import ZoneInfo
        
        now = datetime.now(ZoneInfo("Asia/Tbilisi"))
        time_obj = Time(year=now.year, month=now.month, day=now.day, hour=now.hour, minute=None)
        result = TimeFormatter.format_natural_language(time_obj, timezone="Asia/Tbilisi")
        self.assertEqual(result, "now", "When hour matches current hour, date is today, and no minutes specified, should return 'now'")
    
    def test_format_natural_language_not_now_when_minutes_specified(self):
        from zoneinfo import ZoneInfo
        
        now = datetime.now(ZoneInfo("Asia/Tbilisi"))
        time_obj = Time(year=now.year, month=now.month, day=now.day, hour=now.hour, minute=now.minute)
        result = TimeFormatter.format_natural_language(time_obj, timezone="Asia/Tbilisi")
        self.assertNotEqual(result, "now", "When minutes are specified, should not return 'now'")
    
    def test_format_natural_language_not_now_when_different_hour(self):
        from zoneinfo import ZoneInfo
        
        now = datetime.now(ZoneInfo("Asia/Tbilisi"))
        different_hour = (now.hour + 1) % 24
        time_obj = Time(year=now.year, month=now.month, day=now.day, hour=different_hour, minute=None)
        result = TimeFormatter.format_natural_language(time_obj, timezone="Asia/Tbilisi")
        self.assertNotEqual(result, "now", "When hour is different from current hour, should not return 'now'")
    
    def test_is_relative_date(self):
        today = datetime.now()
        time_obj = Time(year=today.year, month=today.month, day=today.day)
        self.assertTrue(TimeFormatter.is_relative_date(time_obj))
        
        tomorrow = datetime.now() + timedelta(days=1)
        time_obj = Time(year=tomorrow.year, month=tomorrow.month, day=tomorrow.day)
        self.assertTrue(TimeFormatter.is_relative_date(time_obj))
        
        yesterday = datetime.now() - timedelta(days=1)
        time_obj = Time(year=yesterday.year, month=yesterday.month, day=yesterday.day)
        self.assertTrue(TimeFormatter.is_relative_date(time_obj))
        
        far_past = datetime.now() - timedelta(days=10)
        time_obj = Time(year=far_past.year, month=far_past.month, day=far_past.day)
        self.assertFalse(TimeFormatter.is_relative_date(time_obj))




class TestTimeZoneSupport(unittest.TestCase):
    def test_formatter_with_timezone(self):
        from zoneinfo import ZoneInfo
        ny_tz = ZoneInfo("America/New_York")
        ny_now = datetime.now(ny_tz)
        time_obj = Time(year=ny_now.year, month=ny_now.month, day=ny_now.day)
        result = TimeFormatter.format_natural_language(time_obj, timezone="America/New_York")
        self.assertIn(result.lower(), ["today", "tomorrow", "yesterday"], f"Expected relative date, got: {result}")
    
    def test_formatter_yesterday_with_timezone(self):
        from zoneinfo import ZoneInfo
        london_tz = ZoneInfo("Europe/London")
        london_now = datetime.now(london_tz)
        yesterday = london_now - timedelta(days=1)
        time_obj = Time(year=yesterday.year, month=yesterday.month, day=yesterday.day)
        result = TimeFormatter.format_natural_language(time_obj, timezone="Europe/London")
        self.assertEqual(result, "yesterday")
    
    def test_formatter_tomorrow_with_timezone(self):
        from zoneinfo import ZoneInfo
        tokyo_tz = ZoneInfo("Asia/Tokyo")
        tokyo_now = datetime.now(tokyo_tz)
        tomorrow = tokyo_now + timedelta(days=1)
        time_obj = Time(year=tomorrow.year, month=tomorrow.month, day=tomorrow.day)
        result = TimeFormatter.format_natural_language(time_obj, timezone="Asia/Tokyo")
        self.assertEqual(result, "tomorrow")
    
    def test_formatter_with_reference_time_and_timezone(self):
        reference = datetime(2024, 1, 15, 12, 0, 0)
        time_obj = Time(year=2024, month=1, day=15)
        result = TimeFormatter.format_natural_language(time_obj, reference_time=reference, timezone="UTC")
        self.assertEqual(result, "today")
    
    def test_formatter_timezone_naive_reference_time(self):
        reference = datetime(2024, 1, 15, 12, 0, 0)
        time_obj = Time(year=2024, month=1, day=16)
        result = TimeFormatter.format_natural_language(time_obj, reference_time=reference, timezone="America/New_York")
        self.assertEqual(result, "tomorrow")
    
    def test_formatter_format_for_range_with_timezone(self):
        from zoneinfo import ZoneInfo
        ny_tz = ZoneInfo("America/New_York")
        ny_now = datetime.now(ny_tz)
        time_obj = Time(year=ny_now.year, month=ny_now.month, day=ny_now.day)
        result = TimeFormatter.format_for_range(time_obj, timezone="America/New_York")
        self.assertIsNotNone(result)
        self.assertIn("today", result.lower())
    
    def test_formatter_determine_tense_with_timezone(self):
        from zoneinfo import ZoneInfo
        utc_tz = ZoneInfo("UTC")
        utc_now = datetime.now(utc_tz)
        time_obj = Time(year=utc_now.year, month=utc_now.month, day=utc_now.day)
        tense = TimeFormatter.determine_tense(time_obj, timezone="UTC")
        self.assertEqual(tense, Tense.PRESENT)
    
    def test_formatter_determine_tense_past_with_timezone(self):
        from zoneinfo import ZoneInfo
        london_tz = ZoneInfo("Europe/London")
        london_now = datetime.now(london_tz)
        yesterday = london_now - timedelta(days=1)
        time_obj = Time(year=yesterday.year, month=yesterday.month, day=yesterday.day)
        tense = TimeFormatter.determine_tense(time_obj, timezone="Europe/London")
        self.assertEqual(tense, Tense.PAST)
    
    def test_formatter_determine_tense_future_with_timezone(self):
        from zoneinfo import ZoneInfo
        tokyo_tz = ZoneInfo("Asia/Tokyo")
        tokyo_now = datetime.now(tokyo_tz)
        tomorrow = tokyo_now + timedelta(days=1)
        time_obj = Time(year=tomorrow.year, month=tomorrow.month, day=tomorrow.day)
        tense = TimeFormatter.determine_tense(time_obj, timezone="Asia/Tokyo")
        self.assertEqual(tense, Tense.FUTURE)
    
    def test_timezone_with_formatter(self):
        from zoneinfo import ZoneInfo
        
        timezones = ["America/New_York", "Europe/London", "Asia/Tokyo", "UTC"]
        for tz_name in timezones:
            tz = ZoneInfo(tz_name)
            tz_now = datetime.now(tz)
            time_obj = Time(year=tz_now.year, month=tz_now.month, day=tz_now.day)
            result = TimeFormatter.format_natural_language(time_obj, timezone=tz_name)
            self.assertIsNotNone(result)
            self.assertEqual(result, "today")
    
    
    def test_timezone_produces_different_results_ny_vs_london(self):
        from datetime import datetime, timedelta
        from zoneinfo import ZoneInfo
        
        now_ny = datetime.now(ZoneInfo("America/New_York"))
        fixed_time_ny = now_ny - timedelta(hours=3)
        fixed_time = Time(year=fixed_time_ny.year, month=fixed_time_ny.month, day=fixed_time_ny.day, hour=fixed_time_ny.hour, minute=30)
        
        result_ny = TimeFormatter.format_natural_language(fixed_time, timezone="America/New_York")
        result_london = TimeFormatter.format_natural_language(fixed_time, timezone="Europe/London")
        
        self.assertIsNotNone(result_ny)
        self.assertIsNotNone(result_london)
        time_terms_overlap = any(t in result_ny.lower() for t in ["today", "yesterday"])
        self.assertTrue(result_ny != result_london or time_terms_overlap, "NY and London should differ unless both produce today/yesterday")
        
        result_ny_lower = result_ny.lower()
        result_london_lower = result_london.lower()
        time_terms = ["ago", "hour", "minute", "now", "today", "yesterday", "tomorrow", "day", "week", "month", "year", "last", "next", "in"]
        self.assertTrue(
            any(term in result_ny_lower for term in time_terms),
            f"NY result '{result_ny}' should contain time-related terms"
        )
        self.assertTrue(
            any(term in result_london_lower for term in time_terms),
            f"London result '{result_london}' should contain time-related terms"
        )
    
    def test_timezone_with_hours_produces_different_results_ny_vs_london(self):
        from zoneinfo import ZoneInfo
        from unittest.mock import patch
        import analogai.foundation.common.utils.time_utils.timezone_utils as tz_utils

        fixed_date = datetime(2024, 6, 15, 12, 30, 0, tzinfo=ZoneInfo("UTC"))
        reference_ny = datetime(2024, 6, 15, 10, 0, 0, tzinfo=ZoneInfo("America/New_York"))
        fixed_time = Time(year=fixed_date.year, month=fixed_date.month, day=fixed_date.day, hour=fixed_date.hour, minute=fixed_date.minute)

        def _now(tz=None):
            if tz and "New_York" in str(tz):
                return reference_ny
            if tz and "London" in str(tz):
                return reference_ny.astimezone(ZoneInfo("Europe/London"))
            return reference_ny.astimezone(tz) if tz else reference_ny

        with patch.object(tz_utils, "datetime") as mock_dt:
            mock_dt.now.side_effect = _now

            result_ny = TimeFormatter.format_natural_language(fixed_time, timezone="America/New_York")
            result_london = TimeFormatter.format_natural_language(fixed_time, timezone="Europe/London")

        self.assertNotEqual(result_ny, result_london, "NY and London must produce different results when hour/minute specified")
        self.assertIn("hour", result_ny.lower())
        self.assertIn("hour", result_london.lower())
    
    def test_timezone_statement_serializer_ny_vs_london(self):
        from analogai.foundation.core.value_objects.modifier import Modifier
        from analogai.foundation.common.utils.statement_serializers import serialize_statement
        from datetime import datetime, timedelta
        from zoneinfo import ZoneInfo
        
        now_ny = datetime.now(ZoneInfo("America/New_York"))
        fixed_time_ny = now_ny - timedelta(hours=3)
        fixed_time = Time(year=fixed_time_ny.year, month=fixed_time_ny.month, day=fixed_time_ny.day, hour=fixed_time_ny.hour, minute=30)
        modifier = Modifier(from_time=fixed_time)
        
        result_ny = serialize_statement("Human", "mortal", Relation.from_string("is"), 0.8, modifier, timezone="America/New_York")
        result_london = serialize_statement("Human", "mortal", Relation.from_string("is"), 0.8, modifier, timezone="Europe/London")
        
        self.assertIsNotNone(result_ny)
        self.assertIsNotNone(result_london)
        time_terms_overlap = any(t in result_ny.lower() for t in ["today", "yesterday"])
        self.assertTrue(result_ny != result_london or time_terms_overlap, "NY and London should differ unless both produce today/yesterday")
        self.assertIn("human", result_ny.lower())
        self.assertIn("mortal", result_ny.lower())
        self.assertIn("human", result_london.lower())
        self.assertIn("mortal", result_london.lower())
    
    def test_timezone_statement_serializer_with_hours_ny_vs_london(self):
        from analogai.foundation.core.value_objects.modifier import Modifier
        from analogai.foundation.common.utils.statement_serializers import serialize_statement
        from zoneinfo import ZoneInfo
        from unittest.mock import patch
        import analogai.foundation.common.utils.time_utils.timezone_utils as tz_utils

        fixed_date = datetime(2024, 6, 15, 12, 30, 0, tzinfo=ZoneInfo("UTC"))
        reference_ny = datetime(2024, 6, 15, 10, 0, 0, tzinfo=ZoneInfo("America/New_York"))
        fixed_time = Time(year=fixed_date.year, month=fixed_date.month, day=fixed_date.day, hour=fixed_date.hour, minute=fixed_date.minute)
        modifier = Modifier(from_time=fixed_time)

        def _now(tz=None):
            if tz and "New_York" in str(tz):
                return reference_ny
            if tz and "London" in str(tz):
                return reference_ny.astimezone(ZoneInfo("Europe/London"))
            return reference_ny.astimezone(tz) if tz else reference_ny

        with patch.object(tz_utils, "datetime") as mock_dt:
            mock_dt.now.side_effect = _now

            result_ny = serialize_statement("Human", "mortal", Relation.from_string("is"), 0.8, modifier, timezone="America/New_York")
            result_london = serialize_statement("Human", "mortal", Relation.from_string("is"), 0.8, modifier, timezone="Europe/London")

        self.assertNotEqual(result_ny, result_london, "NY and London must produce different results when hour/minute specified")
        self.assertIn("hour", result_ny.lower())
        self.assertIn("hour", result_london.lower())
    


if __name__ == '__main__':
    unittest.main()
