import { describe, expect, it } from 'vitest'
import {
  applyConversion,
  detectDateFormat,
  parseClockifyCsv,
  parseDateOnly,
  parseDurationSeconds,
  parseTimeOnly,
  splitClientProject,
  type TeamLookup,
} from '@/lib/clockify'

describe('parseDateOnly', () => {
  it('CLK-001 parses YYYY-MM-DD', () => {
    expect(parseDateOnly('2026-05-20', 'YYYY-MM-DD')).toBe('2026-05-20')
    expect(parseDateOnly('2026-1-5', 'YYYY-MM-DD')).toBe('2026-01-05')
  })

  it('CLK-002 parses MM/DD/YYYY with US selector', () => {
    expect(parseDateOnly('5/20/2026', 'MM/DD/YYYY')).toBe('2026-05-20')
    expect(parseDateOnly('12/2/2025', 'MM/DD/YYYY')).toBe('2025-12-02')
  })

  it('CLK-003 parses DD/MM/YYYY with non-US selector, same input differs from US', () => {
    expect(parseDateOnly('2/12/2025', 'DD/MM/YYYY')).toBe('2025-12-02')
    expect(parseDateOnly('2/12/2025', 'MM/DD/YYYY')).toBe('2025-02-12')
  })

  it('CLK-004 returns null on garbage', () => {
    expect(parseDateOnly('not a date', 'YYYY-MM-DD')).toBeNull()
    expect(parseDateOnly('', 'MM/DD/YYYY')).toBeNull()
  })

  it('CLK-005 rejects out-of-range month/day', () => {
    expect(parseDateOnly('2026-13-01', 'YYYY-MM-DD')).toBeNull()
    expect(parseDateOnly('2026-02-32', 'YYYY-MM-DD')).toBeNull()
    expect(parseDateOnly('13/01/2026', 'MM/DD/YYYY')).toBeNull()
  })

  it('CLK-006 treats 2-digit years as 2000-series', () => {
    expect(parseDateOnly('5/20/26', 'MM/DD/YYYY')).toBe('2026-05-20')
    expect(parseDateOnly('20/5/26', 'DD/MM/YYYY')).toBe('2026-05-20')
  })
})

describe('parseTimeOnly', () => {
  it('CLK-007 parses 24-hour times', () => {
    expect(parseTimeOnly('13:45:30', '24h')).toBe('13:45:30')
    expect(parseTimeOnly('0:00:00', '24h')).toBe('00:00:00')
    expect(parseTimeOnly('9:05', '24h')).toBe('09:05:00')
  })

  it('CLK-008 parses 12-hour times with AM/PM', () => {
    expect(parseTimeOnly('1:45:30 PM', '12h')).toBe('13:45:30')
    expect(parseTimeOnly('1:45:30 am', '12h')).toBe('01:45:30')
  })

  it('CLK-009 handles 12 AM and 12 PM correctly', () => {
    expect(parseTimeOnly('12:00:00 AM', '12h')).toBe('00:00:00')
    expect(parseTimeOnly('12:00:00 PM', '12h')).toBe('12:00:00')
    expect(parseTimeOnly('12:30:00 AM', '12h')).toBe('00:30:00')
    expect(parseTimeOnly('12:30:00 PM', '12h')).toBe('12:30:00')
  })

  it('CLK-010 returns null on garbage', () => {
    expect(parseTimeOnly('25:00:00', '24h')).toBeNull()
    expect(parseTimeOnly('', '24h')).toBeNull()
    expect(parseTimeOnly('13:45:30 PM', '12h')).toBeNull() // 13 with PM
  })

  // Auto-detect: the fmt argument is now ignored. AM/PM presence
  // disambiguates 12h vs 24h per-string.
  it('CLK-026 24h works regardless of fmt arg', () => {
    expect(parseTimeOnly('14:22:07', '12h')).toBe('14:22:07')
    expect(parseTimeOnly('14:22:07', '24h')).toBe('14:22:07')
    expect(parseTimeOnly('14:22:07')).toBe('14:22:07')
  })
  it('CLK-027 12h works regardless of fmt arg', () => {
    expect(parseTimeOnly('2:22:07 PM', '24h')).toBe('14:22:07')
    expect(parseTimeOnly('2:22:07 PM', '12h')).toBe('14:22:07')
    expect(parseTimeOnly('2:22:07 PM')).toBe('14:22:07')
  })
  it('CLK-028 plain 1:45:30 (no suffix) is 24h, not garbage', () => {
    // Before auto-detect this would have failed against the 12h
    // parser. After: no AM/PM → 24h interpretation.
    expect(parseTimeOnly('1:45:30', '12h')).toBe('01:45:30')
    expect(parseTimeOnly('1:45:30')).toBe('01:45:30')
  })
})

describe('detectDateFormat', () => {
  it('CLK-029 picks ISO when 4-digit-year prefix present', () => {
    expect(detectDateFormat(['2026-05-20', '2026-05-21'])).toBe(
      'YYYY-MM-DD',
    )
  })
  it('CLK-030 picks DD/MM when ANY first slot > 12', () => {
    expect(
      detectDateFormat(['1/5/2026', '25/05/2026', '1/2/2026']),
    ).toBe('DD/MM/YYYY')
  })
  it('CLK-031 picks MM/DD when ANY second slot > 12 (and no DD signal)', () => {
    expect(
      detectDateFormat(['5/20/2026', '6/1/2026', '1/2/2026']),
    ).toBe('MM/DD/YYYY')
  })
  it('CLK-032 returns null when every input is fully ambiguous', () => {
    expect(detectDateFormat(['1/2/2026', '5/6/2026'])).toBeNull()
    expect(detectDateFormat([])).toBeNull()
  })
  it('CLK-033 ignores blank / unparseable entries', () => {
    expect(
      detectDateFormat(['', '   ', 'gibberish', '25/05/2026']),
    ).toBe('DD/MM/YYYY')
  })
  it('CLK-034 mixed CSV: ISO + numeric — first non-null signal wins', () => {
    // Real life: some rows ISO, some DD/MM. Detector should land on
    // one and not crash.
    const out = detectDateFormat(['2026-05-20', '25/05/2026'])
    expect(out === 'YYYY-MM-DD' || out === 'DD/MM/YYYY').toBe(true)
  })
})

describe('parseDurationSeconds', () => {
  it('CLK-011 parses HH:MM:SS', () => {
    expect(parseDurationSeconds('1:00:00')).toBe(3600)
    expect(parseDurationSeconds('0:01:30')).toBe(90)
    expect(parseDurationSeconds('5:20:36')).toBe(5 * 3600 + 20 * 60 + 36)
  })

  it('CLK-012 parses HH:MM (no seconds)', () => {
    expect(parseDurationSeconds('1:30')).toBe(90 * 60)
    expect(parseDurationSeconds('0:30')).toBe(30 * 60)
  })

  it('CLK-013 parses decimal hours', () => {
    expect(parseDurationSeconds('1.5')).toBe(5400)
    expect(parseDurationSeconds('0.25')).toBe(900)
    expect(parseDurationSeconds('5.34')).toBe(Math.round(5.34 * 3600))
  })

  it('returns null for empty / nonsense', () => {
    expect(parseDurationSeconds('')).toBeNull()
    expect(parseDurationSeconds('forever')).toBeNull()
  })
})

describe('parseClockifyCsv', () => {
  const 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)'

  const row = (overrides: Partial<Record<string, string>> = {}) => {
    const fields = {
      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',
      ...overrides,
    }
    return [
      '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)',
    ]
      .map((k) => fields[k as keyof typeof fields])
      .join(',')
  }

  it('CLK-014 returns one entry per non-empty row', () => {
    const csv = [header, row(), row({ Description: 'Other' })].join('\n')
    const r = parseClockifyCsv(csv, 'MM/DD/YYYY', '12h')
    expect(r.entries).toHaveLength(2)
    expect(r.errors).toHaveLength(0)
    expect(r.entries[0].start_at).toBe('2026-05-20 09:00:00')
    expect(r.entries[0].end_at).toBe('2026-05-20 10:30:00')
    expect(r.entries[0].duration_seconds).toBe(5400)
    expect(r.entries[0].billable).toBe(true)
    // CSV "Client" goes into `operator`. CSV "Project" gets split.
    expect(r.entries[0].operator).toBe('Test Client')
    expect(r.entries[0].project).toBe('Test Project')
    expect(r.entries[0].client).toBeNull()
  })

  it('CLK-021 splitClientProject splits on the first " : "', () => {
    expect(splitClientProject('Foo : Bar')).toEqual({
      client: 'Foo',
      project: 'Bar',
    })
    expect(splitClientProject('Foo : Bar : Baz')).toEqual({
      client: 'Foo',
      project: 'Bar : Baz',
    })
    expect(splitClientProject('NoColon')).toEqual({
      client: null,
      project: 'NoColon',
    })
    // No surrounding spaces — strict " : " separator
    expect(splitClientProject('Foo:Bar')).toEqual({
      client: null,
      project: 'Foo:Bar',
    })
  })

  it('CLK-022 parseClockifyCsv splits the Project field into client + project', () => {
    const csv = [
      header,
      row({ Project: 'DaxTech : Website' }),
      row({ Project: 'PromptVictoria' }), // no colon → client null
    ].join('\n')
    const r = parseClockifyCsv(csv, 'MM/DD/YYYY', '12h')
    expect(r.entries[0].client).toBe('DaxTech')
    expect(r.entries[0].project).toBe('Website')
    expect(r.entries[1].client).toBeNull()
    expect(r.entries[1].project).toBe('PromptVictoria')
  })

  it('CLK-015 reports missing required headers', () => {
    const bad = 'Project,Client,Description\n' + 'A,B,C'
    const r = parseClockifyCsv(bad, 'MM/DD/YYYY', '12h')
    expect(r.entries).toHaveLength(0)
    expect(r.errors[0].message).toMatch(/missing required columns/i)
  })

  it('CLK-016 collects unique emails and date range', () => {
    const csv = [
      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' }),
    ].join('\n')
    const r = parseClockifyCsv(csv, 'MM/DD/YYYY', '12h')
    expect(r.uniqueEmails).toEqual(['a@x.com', 'b@x.com'])
    expect(r.dateRange).toEqual({ min: '2026-05-18', max: '2026-05-22' })
  })

  it('CLK-017 normalizes emails to lowercase', () => {
    const csv = [header, row({ Email: 'Mixed.Case@EXAMPLE.com' })].join('\n')
    const r = parseClockifyCsv(csv, 'MM/DD/YYYY', '12h')
    expect(r.entries[0].source_user_email).toBe('mixed.case@example.com')
  })
})

describe('applyConversion', () => {
  type Member = {
    id: string
    display_name: string
    rate_proportion: number
    consolidate_as: string | null
    cost_rate_usd: number | null
    billout_rate_usd: number | null
  }
  const adi: Member = {
    id: 'adi-id',
    display_name: 'Adi Pramono',
    rate_proportion: 1.0,
    consolidate_as: null,
    cost_rate_usd: null,
    billout_rate_usd: null,
  }
  const dev: Member = {
    id: 'dev-id',
    display_name: 'Dev Person',
    rate_proportion: 0.5,
    consolidate_as: 'Tingang',
    cost_rate_usd: null,
    billout_rate_usd: null,
  }
  const lookup: TeamLookup = new Map([
    ['info@adipramono.com', adi],
    ['dev@example.com', dev],
  ])

  const baseEntry = {
    source_user_name: '',
    source_user_email: '',
    operator: 'PlusROI',
    client: 'TestClient',
    project: 'TestProject',
    description: 'd',
    billable: true,
    start_at: '2026-05-20 09:00:00',
    end_at: '2026-05-20 11:00:00',
    duration_seconds: 7200,
    source_rate_usd: null,
    source_amount_usd: null,
  }

  it('CLK-018 returns null fields for an unknown email', () => {
    const r = applyConversion(
      { ...baseEntry, source_user_email: 'unknown@example.com' },
      lookup,
    )
    expect(r).toEqual({
      team_member_id: null,
      converted_user: null,
      converted_duration_seconds: null,
      cost_rate_usd: null,
      billout_rate_usd: null,
    })
  })

  it('CLK-019 uses consolidate_as for devs, display_name for Adi', () => {
    const r1 = applyConversion(
      { ...baseEntry, source_user_email: 'dev@example.com' },
      lookup,
    )
    expect(r1.converted_user).toBe('Tingang')

    const r2 = applyConversion(
      { ...baseEntry, source_user_email: 'info@adipramono.com' },
      lookup,
    )
    expect(r2.converted_user).toBe('Adi Pramono')
  })

  it('CLK-020 multiplies duration by rate proportion', () => {
    const r1 = applyConversion(
      { ...baseEntry, source_user_email: 'dev@example.com' },
      lookup,
    )
    expect(r1.converted_duration_seconds).toBe(3600) // 7200 × 0.5

    const r2 = applyConversion(
      { ...baseEntry, source_user_email: 'info@adipramono.com' },
      lookup,
    )
    expect(r2.converted_duration_seconds).toBe(7200) // 7200 × 1.0
  })

  it('CLK-021 surfaces billout_rate_usd from the lookup', () => {
    const lookupWithRate: TeamLookup = new Map([
      ['info@adipramono.com', { ...adi, billout_rate_usd: 25 }],
    ])
    const r = applyConversion(
      { ...baseEntry, source_user_email: 'info@adipramono.com' },
      lookupWithRate,
    )
    expect(r.billout_rate_usd).toBe(25)
  })
})

describe('computeBilloutAmount', () => {
  it('CLK-022 returns null if either input is null', async () => {
    const { computeBilloutAmount } = await import('@/lib/clockify')
    expect(computeBilloutAmount(null, 25)).toBeNull()
    expect(computeBilloutAmount(3600, null)).toBeNull()
    expect(computeBilloutAmount(null, null)).toBeNull()
  })

  it('CLK-023 = converted_seconds / 3600 × rate, rounded to 2dp', async () => {
    const { computeBilloutAmount } = await import('@/lib/clockify')
    expect(computeBilloutAmount(3600, 25)).toBe(25) // 1h × $25 = $25.00
    expect(computeBilloutAmount(1800, 25)).toBe(12.5) // 0.5h × $25 = $12.50
    expect(computeBilloutAmount(60, 25)).toBeCloseTo(0.42, 2) // 1min × $25 ≈ $0.42
  })
})
