"""T-CLK-* — the legacy clockify.test.ts fixture suite, ported 1:1
(06-test-plan §2.1). Fixture values are VERBATIM from
/srv/apps/work/tests/clockify.test.ts; the CLK-021/022 duplicate IDs
are renumbered 021a/022a here (021b/022b live in tests/money — the
stamping side). CLK-018/019/020 (applyConversion) moved: 018's
"unknown email = unresolved" survives in tests/imports/test_commit.py;
019 (consolidate_as) and 020 (rate_proportion) are phase-outs and were
deliberately NOT ported (06 §1 dropped list)."""

from app.services.imports.clockify import (
    detect_date_format,
    format_date_range_label,
    parse_clockify_csv,
    parse_date_only,
    parse_duration_seconds,
    parse_time_only,
    split_client_project,
)

# ---------------------------------------------------------------- dates


def test_clk_001_parses_iso():
    assert parse_date_only("2026-05-20", "YYYY-MM-DD") == "2026-05-20"
    assert parse_date_only("2026-1-5", "YYYY-MM-DD") == "2026-01-05"


def test_clk_002_parses_mmdd_with_us_selector():
    assert parse_date_only("5/20/2026", "MM/DD/YYYY") == "2026-05-20"
    assert parse_date_only("12/2/2025", "MM/DD/YYYY") == "2025-12-02"


def test_clk_003_ddmm_selector_differs_from_us():
    assert parse_date_only("2/12/2025", "DD/MM/YYYY") == "2025-12-02"
    assert parse_date_only("2/12/2025", "MM/DD/YYYY") == "2025-02-12"


def test_clk_004_null_on_garbage():
    assert parse_date_only("not a date", "YYYY-MM-DD") is None
    assert parse_date_only("", "MM/DD/YYYY") is None


def test_clk_005_rejects_out_of_range():
    assert parse_date_only("2026-13-01", "YYYY-MM-DD") is None
    assert parse_date_only("2026-02-32", "YYYY-MM-DD") is None
    assert parse_date_only("13/01/2026", "MM/DD/YYYY") is None


def test_clk_006_two_digit_years_are_2000s():
    assert parse_date_only("5/20/26", "MM/DD/YYYY") == "2026-05-20"
    assert parse_date_only("20/5/26", "DD/MM/YYYY") == "2026-05-20"


# ---------------------------------------------------------------- times


def test_clk_007_parses_24h():
    assert parse_time_only("13:45:30", "24h") == "13:45:30"
    assert parse_time_only("0:00:00", "24h") == "00:00:00"
    assert parse_time_only("9:05", "24h") == "09:05:00"


def test_clk_008_parses_12h_with_meridiem():
    assert parse_time_only("1:45:30 PM", "12h") == "13:45:30"
    assert parse_time_only("1:45:30 am", "12h") == "01:45:30"


def test_clk_009_noon_and_midnight():
    assert parse_time_only("12:00:00 AM", "12h") == "00:00:00"
    assert parse_time_only("12:00:00 PM", "12h") == "12:00:00"
    assert parse_time_only("12:30:00 AM", "12h") == "00:30:00"
    assert parse_time_only("12:30:00 PM", "12h") == "12:30:00"


def test_clk_010_null_on_garbage():
    assert parse_time_only("25:00:00", "24h") is None
    assert parse_time_only("", "24h") is None
    assert parse_time_only("13:45:30 PM", "12h") is None  # 13 with PM


def test_clk_026_24h_regardless_of_fmt_arg():
    assert parse_time_only("14:22:07", "12h") == "14:22:07"
    assert parse_time_only("14:22:07", "24h") == "14:22:07"
    assert parse_time_only("14:22:07") == "14:22:07"


def test_clk_027_12h_regardless_of_fmt_arg():
    assert parse_time_only("2:22:07 PM", "24h") == "14:22:07"
    assert parse_time_only("2:22:07 PM", "12h") == "14:22:07"
    assert parse_time_only("2:22:07 PM") == "14:22:07"


def test_clk_028_plain_low_hour_is_24h_not_garbage():
    assert parse_time_only("1:45:30", "12h") == "01:45:30"
    assert parse_time_only("1:45:30") == "01:45:30"


# ------------------------------------------------------ format detection


def test_clk_029_iso_on_four_digit_year_prefix():
    assert detect_date_format(["2026-05-20", "2026-05-21"]) == "YYYY-MM-DD"


def test_clk_030_ddmm_when_any_first_slot_over_12():
    assert detect_date_format(["1/5/2026", "25/05/2026", "1/2/2026"]) == "DD/MM/YYYY"


def test_clk_031_mmdd_when_any_second_slot_over_12():
    assert detect_date_format(["5/20/2026", "6/1/2026", "1/2/2026"]) == "MM/DD/YYYY"


def test_clk_032_null_when_nothing_parses():
    assert detect_date_format([]) is None
    assert detect_date_format(["gibberish", "also gibberish"]) is None


def test_clk_032b_tightest_span_week_is_ddmm():
    # Real failure mode 2026-06-09 (the "26-01-08" batch): every
    # day-of-month <= 12 scattered 132 June entries across 8 months.
    week = ["01/06/2026", "02/06/2026", "03/06/2026", "05/06/2026", "08/06/2026"]
    assert detect_date_format(week) == "DD/MM/YYYY"


def test_clk_032c_tightest_span_us_weekly_export():
    week = ["06/01/2026", "06/02/2026", "06/03/2026", "06/05/2026", "06/08/2026"]
    assert detect_date_format(week) == "MM/DD/YYYY"


def test_clk_032d_null_when_spans_equal():
    assert detect_date_format(["1/2/2026", "2/1/2026"]) is None


def test_clk_033_ignores_blank_and_unparseable():
    assert detect_date_format(["", "   ", "gibberish", "25/05/2026"]) == "DD/MM/YYYY"


def test_clk_034_mixed_iso_and_numeric_first_signal_wins():
    out = detect_date_format(["2026-05-20", "25/05/2026"])
    assert out in ("YYYY-MM-DD", "DD/MM/YYYY")


# ----------------------------------------------------- date-range labels


def test_clk_040_same_month_range():
    assert (
        format_date_range_label({"min": "2026-06-02", "max": "2026-06-08"})
        == "Jun 2 – Jun 8, 2026"
    )


def test_clk_041_cross_month_same_year():
    assert (
        format_date_range_label({"min": "2026-05-28", "max": "2026-06-03"})
        == "May 28 – Jun 3, 2026"
    )


def test_clk_042_cross_year_shows_both_years():
    assert (
        format_date_range_label({"min": "2025-12-28", "max": "2026-01-03"})
        == "Dec 28, 2025 – Jan 3, 2026"
    )


def test_clk_043_single_day():
    assert format_date_range_label({"min": "2026-06-02", "max": "2026-06-02"}) == "Jun 2, 2026"


def test_clk_044_null_in_null_out():
    assert format_date_range_label(None) is None


# ------------------------------------------------------------- durations


def test_clk_011_parses_hms():
    assert parse_duration_seconds("1:00:00") == 3600
    assert parse_duration_seconds("0:01:30") == 90
    assert parse_duration_seconds("5:20:36") == 5 * 3600 + 20 * 60 + 36


def test_clk_012_parses_hm():
    assert parse_duration_seconds("1:30") == 90 * 60
    assert parse_duration_seconds("0:30") == 30 * 60


def test_clk_013_parses_decimal_hours():
    assert parse_duration_seconds("1.5") == 5400
    assert parse_duration_seconds("0.25") == 900
    assert parse_duration_seconds("5.34") == round(5.34 * 3600)


def test_clk_013b_null_for_empty_or_nonsense():
    assert parse_duration_seconds("") is None
    assert parse_duration_seconds("forever") is None


# ------------------------------------------------------------- whole-CSV

HEADER = (
    "Project,Client,Description,Task,User,Group,Email,Tags,Billable,"
    "Start Date,Start Time,End Date,End Time,Duration (h),Duration (decimal),"
    "Billable Rate (USD),Billable Amount (USD)"
)

FIELD_ORDER = (
    "Project", "Client", "Description", "Task", "User", "Group", "Email", "Tags",
    "Billable", "Start Date", "Start Time", "End Date", "End Time", "Duration (h)",
    "Duration (decimal)", "Billable Rate (USD)", "Billable Amount (USD)",
)

DEFAULTS = {
    "Project": "Test Project",
    "Client": "Test Client",
    "Description": "Something",
    "Task": "",
    "User": "Test User",
    "Group": "",
    "Email": "TEST@example.com",
    "Tags": "",
    "Billable": "Yes",
    "Start Date": "5/20/2026",
    "Start Time": "9:00:00 AM",
    "End Date": "5/20/2026",
    "End Time": "10:30:00 AM",
    "Duration (h)": "1:30:00",
    "Duration (decimal)": "1.5",
    "Billable Rate (USD)": "14",
    "Billable Amount (USD)": "21.00",
}


def row(**overrides: str) -> str:
    fields = {**DEFAULTS, **overrides}
    return ",".join(fields[key] for key in FIELD_ORDER)


def test_clk_014_one_entry_per_non_empty_row():
    csv = "\n".join([HEADER, row(), row(Description="Other")])
    r = parse_clockify_csv(csv, "MM/DD/YYYY", "12h")
    assert len(r.entries) == 2
    assert r.errors == []
    assert r.entries[0].start_at == "2026-05-20 09:00:00"
    assert r.entries[0].end_at == "2026-05-20 10:30:00"
    assert r.entries[0].duration_seconds == 5400
    assert r.entries[0].billable is True
    # CSV "Client" goes into `operator`; CSV "Project" gets split.
    assert r.entries[0].operator == "Test Client"
    assert r.entries[0].project == "Test Project"
    assert r.entries[0].client is None


def test_clk_021a_split_client_project_on_first_separator():
    assert split_client_project("Foo : Bar") == ("Foo", "Bar")
    assert split_client_project("Foo : Bar : Baz") == ("Foo", "Bar : Baz")
    assert split_client_project("NoColon") == (None, "NoColon")
    # no surrounding spaces — strict " : " separator
    assert split_client_project("Foo:Bar") == (None, "Foo:Bar")


def test_clk_022a_csv_level_split_of_project_field():
    csv = "\n".join(
        [HEADER, row(Project="DaxTech : Website"), row(Project="PromptVictoria")]
    )
    r = parse_clockify_csv(csv, "MM/DD/YYYY", "12h")
    assert r.entries[0].client == "DaxTech"
    assert r.entries[0].project == "Website"
    assert r.entries[1].client is None
    assert r.entries[1].project == "PromptVictoria"


def test_clk_015_reports_missing_required_headers():
    bad = "Project,Client,Description\nA,B,C"
    r = parse_clockify_csv(bad, "MM/DD/YYYY", "12h")
    assert r.entries == []
    assert "missing required columns" in r.errors[0]["message"].lower()


def test_clk_016_unique_emails_and_date_range():
    csv = "\n".join(
        [
            HEADER,
            row(Email="a@x.com", **{"Start Date": "5/20/2026"}),
            row(Email="b@x.com", **{"Start Date": "5/22/2026"}),
            row(Email="a@x.com", **{"Start Date": "5/18/2026"}),
        ]
    )
    r = parse_clockify_csv(csv, "MM/DD/YYYY", "12h")
    assert r.unique_emails == ["a@x.com", "b@x.com"]
    assert r.date_range == {"min": "2026-05-18", "max": "2026-05-22"}


def test_clk_017_emails_lowercased():
    csv = "\n".join([HEADER, row(Email="Mixed.Case@EXAMPLE.com")])
    r = parse_clockify_csv(csv, "MM/DD/YYYY", "12h")
    assert r.entries[0].source_user_email == "mixed.case@example.com"


# ------------------------------------------------- rebuild-only behavior


def test_imp_009_no_toggl_alias_mapping():
    """T-IMP-009 (port*): the Toggl dialect is retired (judgment #8) —
    Member/Stop date/Stop time headers do NOT satisfy the required set."""
    toggl_header = (
        "Project,Client,Description,Email,Member,Start date,Start time,"
        "Stop date,Stop time,Duration"
    )
    toggl_row = "A,B,C,d@x.com,M,2026-05-20,09:00,2026-05-20,10:00,1:00:00"
    r = parse_clockify_csv(toggl_header + "\n" + toggl_row)
    assert r.entries == []
    assert "missing required columns" in r.errors[0]["message"].lower()
    # ... while the Clockify default layout passes (same required set)
    ok = parse_clockify_csv("\n".join([HEADER, row()]))
    assert len(ok.entries) == 1 and ok.errors == []


def test_imp_010_billable_boolean_variants():
    csv = "\n".join(
        [
            HEADER,
            row(Billable="yes"),
            row(Billable="TRUE"),
            row(Billable="1"),
            row(Billable="y"),
            row(Billable="no"),
        ]
    )
    r = parse_clockify_csv(csv, "MM/DD/YYYY", "12h")
    assert [e.billable for e in r.entries] == [True, True, True, True, False]


def test_imp_011_invalid_start_skips_with_error_invalid_end_warns():
    csv = "\n".join(
        [
            HEADER,
            row(**{"Start Date": "garbage"}),
            row(**{"End Time": "99:99"}),
        ]
    )
    r = parse_clockify_csv(csv, "MM/DD/YYYY", "12h")
    # invalid START: row skipped, exact legacy error format
    assert len(r.entries) == 1
    assert r.errors[0]["message"] == "Row 2: invalid start date/time (garbage 9:00:00 AM)"
    # invalid END: row kept, end nulled — and SURFACED (rebuild fixes wart #1)
    assert r.entries[0].end_at is None
    assert any("invalid end date/time" in w["message"] for w in r.warnings)
    assert r.total_rows == 2


def test_imp_030_ambiguous_weekly_csv_lands_in_one_month():
    """The "26-01-08" incident regression: a fully-ambiguous DD/MM
    weekly CSV parsed with the MM/DD fallback still lands in ONE month
    because the tightest-span detector overrides the fallback."""
    days = ["01/06/2026", "02/06/2026", "03/06/2026", "05/06/2026", "08/06/2026"]
    csv = "\n".join([HEADER] + [row(**{"Start Date": d, "End Date": d}) for d in days])
    r = parse_clockify_csv(csv, "MM/DD/YYYY", "12h")  # wrong fallback on purpose
    months = {e.start_at[:7] for e in r.entries}
    assert months == {"2026-06"}
    assert r.date_format_used == "DD/MM/YYYY"


def test_blank_lines_skipped_and_total_rows_counts_data_rows():
    csv = "\n".join([HEADER, row(), "", row(), ""])
    r = parse_clockify_csv(csv, "MM/DD/YYYY", "12h")
    assert len(r.entries) == 2
    assert r.total_rows == 2  # T-IMP-029's numerator: parsed-entry basis
