defmodule ToweropsWeb.TimeHelpersTest do use ExUnit.Case, async: true alias ToweropsWeb.TimeHelpers describe "to_user_timezone/2" do test "returns shifted datetime and abbreviation for known timezone" do datetime = ~U[2026-01-15 14:34:00Z] assert {:ok, shifted, "EST"} = TimeHelpers.to_user_timezone(datetime, "America/New_York") # UTC-5 => 09:34 assert shifted.hour == 9 assert shifted.minute == 34 end test "returns unchanged datetime for UTC" do datetime = ~U[2026-01-15 14:34:00Z] assert {:ok, shifted, "UTC"} = TimeHelpers.to_user_timezone(datetime, "UTC") assert shifted.hour == 14 assert shifted.minute == 34 end test "handles positive offset timezone (Asia/Tokyo, UTC+9)" do datetime = ~U[2026-01-15 14:00:00Z] assert {:ok, shifted, "JST"} = TimeHelpers.to_user_timezone(datetime, "Asia/Tokyo") assert shifted.hour == 23 end test "handles date rollover forward (Asia/Tokyo near midnight)" do datetime = ~U[2026-01-15 20:00:00Z] assert {:ok, shifted, "JST"} = TimeHelpers.to_user_timezone(datetime, "Asia/Tokyo") # 20:00 + 9h = 05:00 next day assert shifted.hour == 5 assert shifted.day == 16 end test "handles date rollover backward (America/Los_Angeles early UTC)" do datetime = ~U[2026-01-15 02:00:00Z] assert {:ok, shifted, "PST"} = TimeHelpers.to_user_timezone(datetime, "America/Los_Angeles") # 02:00 - 8h = 18:00 previous day assert shifted.hour == 18 assert shifted.day == 14 end test "returns error for unknown timezone" do datetime = ~U[2026-01-15 14:34:00Z] assert {:error, :unknown_timezone} = TimeHelpers.to_user_timezone(datetime, "Invalid/Zone") end test "handles all supported timezones without error" do datetime = ~U[2026-01-15 12:00:00Z] timezones = [ "UTC", "America/New_York", "America/Chicago", "America/Denver", "America/Los_Angeles", "America/Phoenix", "Europe/London", "Europe/Paris", "Europe/Berlin", "Asia/Tokyo", "Asia/Shanghai", "Asia/Dubai", "Australia/Sydney" ] for tz <- timezones do assert {:ok, _shifted, _abbr} = TimeHelpers.to_user_timezone(datetime, tz) end end end describe "format_time_ago/1" do test "returns 'Never' for nil datetime" do assert TimeHelpers.format_time_ago(nil) == "Never" end test "formats seconds ago" do datetime = DateTime.add(DateTime.utc_now(), -30, :second) result = TimeHelpers.format_time_ago(datetime) assert result =~ "s ago" end test "formats 0 seconds ago for very recent datetime" do datetime = DateTime.utc_now() result = TimeHelpers.format_time_ago(datetime) assert result =~ "s ago" end test "formats minutes ago" do datetime = DateTime.add(DateTime.utc_now(), -120, :second) result = TimeHelpers.format_time_ago(datetime) assert result == "2m ago" end test "formats 1 minute ago at 60 seconds" do datetime = DateTime.add(DateTime.utc_now(), -65, :second) result = TimeHelpers.format_time_ago(datetime) assert result == "1m ago" end test "formats hours ago" do datetime = DateTime.add(DateTime.utc_now(), -7200, :second) result = TimeHelpers.format_time_ago(datetime) assert result == "2h ago" end test "formats 1 hour ago at 3600 seconds" do datetime = DateTime.add(DateTime.utc_now(), -3600, :second) result = TimeHelpers.format_time_ago(datetime) assert result == "1h ago" end test "formats days ago" do datetime = DateTime.add(DateTime.utc_now(), -172_800, :second) result = TimeHelpers.format_time_ago(datetime) assert result == "2d ago" end test "formats 1 day ago at 86400 seconds" do datetime = DateTime.add(DateTime.utc_now(), -86_400, :second) result = TimeHelpers.format_time_ago(datetime) assert result == "1d ago" end test "formats months ago" do # 60 days = 5,184,000 seconds datetime = DateTime.add(DateTime.utc_now(), -5_184_000, :second) result = TimeHelpers.format_time_ago(datetime) assert result == "2mo ago" end test "formats 1 month ago at 30 days" do # 30 days = 2,592,000 seconds datetime = DateTime.add(DateTime.utc_now(), -2_592_000, :second) result = TimeHelpers.format_time_ago(datetime) assert result == "1mo ago" end test "formats years ago" do # 2 years = 63,072,000 seconds datetime = DateTime.add(DateTime.utc_now(), -63_072_000, :second) result = TimeHelpers.format_time_ago(datetime) assert result == "2y ago" end test "formats 1 year ago at 365 days" do # 365 days = 31,536,000 seconds datetime = DateTime.add(DateTime.utc_now(), -31_536_000, :second) result = TimeHelpers.format_time_ago(datetime) assert result == "1y ago" end test "handles future datetime by treating it as just now (clock skew)" do # Simulate clock skew where datetime is in the future datetime = DateTime.add(DateTime.utc_now(), 5, :second) result = TimeHelpers.format_time_ago(datetime) # Should not crash and should show "0s ago" or similar assert result =~ "s ago" end test "handles future datetime by many seconds (edge case)" do # Extreme clock skew - datetime 30 seconds in the future datetime = DateTime.add(DateTime.utc_now(), 30, :second) result = TimeHelpers.format_time_ago(datetime) # Should clamp to 0 and show as recent assert result =~ "s ago" end end describe "format_datetime/1" do test "formats datetime in UTC with 24h format by default" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_datetime(datetime) == "Jan 15, 2026 at 14:34:00 UTC" end test "returns 'Never' for nil" do assert TimeHelpers.format_datetime(nil) == "Never" end end describe "format_datetime/2" do test "formats datetime in UTC when timezone is nil" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_datetime(datetime, nil) == "Jan 15, 2026 at 14:34:00 UTC" end test "formats datetime in UTC when timezone is explicitly UTC" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_datetime(datetime, "UTC") == "Jan 15, 2026 at 14:34:00 UTC" end test "converts datetime to America/New_York timezone" do datetime = ~U[2026-01-15 14:34:00Z] result = TimeHelpers.format_datetime(datetime, "America/New_York") assert result == "Jan 15, 2026 at 09:34:00 EST" end test "converts datetime to America/Los_Angeles timezone" do datetime = ~U[2026-01-15 14:34:00Z] result = TimeHelpers.format_datetime(datetime, "America/Los_Angeles") assert result == "Jan 15, 2026 at 06:34:00 PST" end test "falls back to UTC for invalid timezone" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_datetime(datetime, "Invalid/Timezone") == "Jan 15, 2026 at 14:34:00 UTC" end test "returns 'Never' for nil datetime" do assert TimeHelpers.format_datetime(nil, "America/New_York") == "Never" assert TimeHelpers.format_datetime(nil, nil) == "Never" end end describe "format_datetime/3" do test "formats datetime in 12h format" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_datetime(datetime, "UTC", "12h") == "Jan 15, 2026 at 02:34 PM UTC" end test "formats datetime in 24h format" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_datetime(datetime, "UTC", "24h") == "Jan 15, 2026 at 14:34:00 UTC" end test "formats datetime in 24h format with timezone conversion" do datetime = ~U[2026-01-15 14:34:00Z] result = TimeHelpers.format_datetime(datetime, "America/New_York", "24h") assert result == "Jan 15, 2026 at 09:34:00 EST" end test "formats datetime in 12h format with timezone conversion" do datetime = ~U[2026-01-15 14:34:00Z] result = TimeHelpers.format_datetime(datetime, "America/New_York", "12h") assert result == "Jan 15, 2026 at 09:34 AM EST" end test "returns 'Never' for nil datetime" do assert TimeHelpers.format_datetime(nil, "UTC", "24h") == "Never" assert TimeHelpers.format_datetime(nil, "UTC", "12h") == "Never" end test "handles nil timezone by defaulting to UTC" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_datetime(datetime, nil, "24h") == "Jan 15, 2026 at 14:34:00 UTC" end test "falls back to UTC for invalid timezone in 24h format" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_datetime(datetime, "Fake/Zone", "24h") == "Jan 15, 2026 at 14:34:00 UTC" end test "formats midnight correctly in 12h format" do datetime = ~U[2026-01-15 00:00:00Z] assert TimeHelpers.format_datetime(datetime, "UTC", "12h") == "Jan 15, 2026 at 12:00 AM UTC" end test "formats noon correctly in 12h format" do datetime = ~U[2026-01-15 12:00:00Z] assert TimeHelpers.format_datetime(datetime, "UTC", "12h") == "Jan 15, 2026 at 12:00 PM UTC" end end describe "format_date/1" do test "formats date in UTC by default" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_date(datetime) == "Jan 15, 2026" end test "returns 'Never' for nil" do assert TimeHelpers.format_date(nil) == "Never" end end describe "format_date/2" do test "formats date in UTC when timezone is nil" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_date(datetime, nil) == "Jan 15, 2026" end test "formats date in UTC when timezone is explicitly UTC" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_date(datetime, "UTC") == "Jan 15, 2026" end test "converts to different date in America/Los_Angeles for early UTC times" do datetime = ~U[2026-01-15 02:00:00Z] assert TimeHelpers.format_date(datetime, "America/Los_Angeles") == "Jan 14, 2026" end test "keeps same date for late UTC times in America/Los_Angeles" do datetime = ~U[2026-01-15 20:00:00Z] assert TimeHelpers.format_date(datetime, "America/Los_Angeles") == "Jan 15, 2026" end test "rolls date forward for positive offset timezones near midnight" do # 23:00 UTC on Jan 15 + 9h (Tokyo) = 08:00 Jan 16 datetime = ~U[2026-01-15 23:00:00Z] assert TimeHelpers.format_date(datetime, "Asia/Tokyo") == "Jan 16, 2026" end test "falls back to UTC for invalid timezone" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_date(datetime, "Invalid/Timezone") == "Jan 15, 2026" end test "returns 'Never' for nil datetime" do assert TimeHelpers.format_date(nil, "America/New_York") == "Never" assert TimeHelpers.format_date(nil, nil) == "Never" end end describe "format_iso8601/1" do test "formats datetime in UTC with 24h format by default" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_iso8601(datetime) == "2026-01-15 14:34:00 UTC" end test "returns 'Never' for nil" do assert TimeHelpers.format_iso8601(nil) == "Never" end end describe "format_iso8601/2" do test "formats datetime in UTC when timezone is nil" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_iso8601(datetime, nil) == "2026-01-15 14:34:00 UTC" end test "formats datetime in UTC when timezone is explicitly UTC" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_iso8601(datetime, "UTC") == "2026-01-15 14:34:00 UTC" end test "converts datetime to America/New_York timezone" do datetime = ~U[2026-01-15 14:34:00Z] result = TimeHelpers.format_iso8601(datetime, "America/New_York") assert result == "2026-01-15 09:34:00 EST" end test "converts datetime to America/Los_Angeles timezone" do datetime = ~U[2026-01-15 14:34:00Z] result = TimeHelpers.format_iso8601(datetime, "America/Los_Angeles") assert result == "2026-01-15 06:34:00 PST" end test "falls back to UTC for invalid timezone" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_iso8601(datetime, "Invalid/Timezone") == "2026-01-15 14:34:00 UTC" end test "returns 'Never' for nil datetime" do assert TimeHelpers.format_iso8601(nil, "America/New_York") == "Never" assert TimeHelpers.format_iso8601(nil, nil) == "Never" end end describe "format_iso8601/3" do test "formats datetime in 24h format" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_iso8601(datetime, "UTC", "24h") == "2026-01-15 14:34:00 UTC" end test "formats datetime in 12h format" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_iso8601(datetime, "UTC", "12h") == "2026-01-15 02:34 PM UTC" end test "formats datetime in 12h format with timezone conversion" do datetime = ~U[2026-01-15 14:34:00Z] result = TimeHelpers.format_iso8601(datetime, "America/New_York", "12h") assert result == "2026-01-15 09:34 AM EST" end test "formats datetime in 24h format with timezone conversion" do datetime = ~U[2026-01-15 14:34:00Z] result = TimeHelpers.format_iso8601(datetime, "America/New_York", "24h") assert result == "2026-01-15 09:34:00 EST" end test "returns 'Never' for nil datetime" do assert TimeHelpers.format_iso8601(nil, "UTC", "24h") == "Never" assert TimeHelpers.format_iso8601(nil, "UTC", "12h") == "Never" end test "handles nil timezone by defaulting to UTC" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_iso8601(datetime, nil, "12h") == "2026-01-15 02:34 PM UTC" end test "falls back to UTC for invalid timezone in 12h format" do datetime = ~U[2026-01-15 14:34:00Z] assert TimeHelpers.format_iso8601(datetime, "Fake/Zone", "12h") == "2026-01-15 02:34 PM UTC" end end describe "format_utc_hour/1" do test "formats hour in UTC with AM" do result = TimeHelpers.format_utc_hour(7) assert result == "7:00 AM UTC" end test "formats midnight as 12 AM" do result = TimeHelpers.format_utc_hour(0) assert result == "12:00 AM UTC" end test "formats noon as 12 PM" do result = TimeHelpers.format_utc_hour(12) assert result == "12:00 PM UTC" end test "formats afternoon hours with PM" do result = TimeHelpers.format_utc_hour(15) assert result == "3:00 PM UTC" end test "formats hour 23 correctly" do result = TimeHelpers.format_utc_hour(23) assert result == "11:00 PM UTC" end end describe "format_utc_hour/2" do test "converts UTC hour to America/New_York" do # 7 UTC - 5h = 2 AM EST result = TimeHelpers.format_utc_hour(7, "America/New_York") assert result == "2:00 AM EST" end test "converts UTC hour to Asia/Tokyo" do # 7 UTC + 9h = 16:00 JST = 4 PM result = TimeHelpers.format_utc_hour(7, "Asia/Tokyo") assert result == "4:00 PM JST" end test "converts midnight UTC to America/Los_Angeles" do # 0 UTC - 8h = 16:00 previous day PST = 4 PM result = TimeHelpers.format_utc_hour(0, "America/Los_Angeles") assert result == "4:00 PM PST" end test "converts noon UTC to Europe/Paris" do # 12 UTC + 1h = 13:00 CET = 1 PM result = TimeHelpers.format_utc_hour(12, "Europe/Paris") assert result == "1:00 PM CET" end test "converts UTC hour to Australia/Sydney" do # 14 UTC + 10h = 24 => 0 next day = 12 AM result = TimeHelpers.format_utc_hour(14, "Australia/Sydney") assert result == "12:00 AM AEST" end test "falls back to UTC for invalid timezone" do result = TimeHelpers.format_utc_hour(7, "Invalid/Zone") assert result == "7:00 AM UTC" end test "handles UTC explicitly" do result = TimeHelpers.format_utc_hour(15, "UTC") assert result == "3:00 PM UTC" end end describe "format_utc_hour/3" do test "formats hour in 24h format" do result = TimeHelpers.format_utc_hour(15, "UTC", "24h") assert result == "15:00 UTC" end test "formats midnight in 24h format" do result = TimeHelpers.format_utc_hour(0, "UTC", "24h") assert result == "0:00 UTC" end test "formats hour in 12h format" do result = TimeHelpers.format_utc_hour(15, "UTC", "12h") assert result == "3:00 PM UTC" end test "formats hour in 24h format with timezone conversion" do result = TimeHelpers.format_utc_hour(7, "America/New_York", "24h") assert result == "2:00 EST" end end describe "format_datetime_for_scope/2" do test "formats datetime using scope timezone and time_format" do scope = %{timezone: "America/New_York", time_format: "12h"} datetime = ~U[2026-01-15 14:34:00Z] result = TimeHelpers.format_datetime_for_scope(datetime, scope) assert result == "Jan 15, 2026 at 09:34 AM EST" end test "formats datetime with 24h scope" do scope = %{timezone: "UTC", time_format: "24h"} datetime = ~U[2026-01-15 14:34:00Z] result = TimeHelpers.format_datetime_for_scope(datetime, scope) assert result == "Jan 15, 2026 at 14:34:00 UTC" end test "returns 'Never' for nil datetime" do scope = %{timezone: "UTC", time_format: "24h"} assert TimeHelpers.format_datetime_for_scope(nil, scope) == "Never" end end end