import { describe, expect, it } from 'vitest';

import { formatDecimal, formatNumber, formatPercent, formatPercentRaw, truncateUrl } from './format';

describe('formatNumber', () => {
  it('formats a normal integer with commas', () => {
    // 1234 → "1,234" (manually computed)
    expect(formatNumber(1234)).toBe('1,234');
  });

  it('returns "—" for NaN', () => {
    expect(formatNumber(NaN)).toBe('—');
  });

  it('returns "—" for positive Infinity', () => {
    expect(formatNumber(Infinity)).toBe('—');
  });

  it('returns "—" for negative Infinity', () => {
    expect(formatNumber(-Infinity)).toBe('—');
  });
});

describe('formatPercent', () => {
  it('multiplies by 100 and appends %', () => {
    // 0.455 × 100 = 45.5 → "45.5%" (manually computed)
    expect(formatPercent(0.455)).toBe('45.5%');
  });

  it('respects custom decimals', () => {
    // 0.1 × 100 = 10.00 with 2 decimals → "10.00%"
    expect(formatPercent(0.1, 2)).toBe('10.00%');
  });

  it('returns "—" for NaN', () => {
    expect(formatPercent(NaN)).toBe('—');
  });

  it('returns "—" for positive Infinity', () => {
    expect(formatPercent(Infinity)).toBe('—');
  });

  it('returns "—" for negative Infinity', () => {
    expect(formatPercent(-Infinity)).toBe('—');
  });
});

describe('formatPercentRaw', () => {
  it('appends % without multiplying', () => {
    // 5.2 with 1 decimal → "5.2%" (no multiplication)
    expect(formatPercentRaw(5.2)).toBe('5.2%');
  });

  it('respects custom decimals', () => {
    // 5.2 with 2 decimals → "5.20%"
    expect(formatPercentRaw(5.2, 2)).toBe('5.20%');
  });

  it('returns "—" for NaN', () => {
    expect(formatPercentRaw(NaN)).toBe('—');
  });

  it('returns "—" for positive Infinity', () => {
    expect(formatPercentRaw(Infinity)).toBe('—');
  });

  it('returns "—" for negative Infinity', () => {
    expect(formatPercentRaw(-Infinity)).toBe('—');
  });
});

describe('formatDecimal', () => {
  it('formats with default 2 decimal places', () => {
    // 3.14159 with 2 decimals → "3.14" (manually computed via Intl.NumberFormat truncation)
    expect(formatDecimal(3.14159)).toBe('3.14');
  });

  it('respects custom decimals', () => {
    // 3.14159 with 4 decimals → "3.1416"
    expect(formatDecimal(3.14159, 4)).toBe('3.1416');
  });

  it('returns "—" for NaN', () => {
    expect(formatDecimal(NaN)).toBe('—');
  });

  it('returns "—" for positive Infinity', () => {
    expect(formatDecimal(Infinity)).toBe('—');
  });

  it('returns "—" for negative Infinity', () => {
    expect(formatDecimal(-Infinity)).toBe('—');
  });
});

describe('truncateUrl', () => {
  it('extracts the path from a full URL', () => {
    expect(truncateUrl('https://example.com/some/path', 100)).toBe('/some/path');
  });

  it('includes query string in the extracted path', () => {
    expect(truncateUrl('https://example.com/page?q=hello', 100)).toBe('/page?q=hello');
  });

  it('truncates the path when it exceeds maxLen', () => {
    const url = 'https://example.com/' + 'a'.repeat(60);
    // path = '/aaa...' (61 chars), maxLen = 50 → slice(0, 50) + '…'
    const result = truncateUrl(url, 50);
    expect(result).toBe('/' + 'a'.repeat(49) + '…');
  });

  it('does not truncate when path equals maxLen exactly', () => {
    // path has 10 chars — "/123456789"
    const url = 'https://example.com/123456789';
    expect(truncateUrl(url, 10)).toBe('/123456789');
  });

  it('uses default maxLen of 50 when no second argument is supplied', () => {
    const longPath = '/p/' + 'x'.repeat(60); // 63 chars
    const url = 'https://example.com' + longPath;
    const result = truncateUrl(url);
    expect(result.length).toBe(51); // 50 chars + '…'
    expect(result.endsWith('…')).toBe(true);
  });

  it('falls back gracefully for non-URL strings', () => {
    expect(truncateUrl('/just/a/path', 100)).toBe('/just/a/path');
  });

  it('truncates plain non-URL strings that exceed maxLen', () => {
    const plain = 'x'.repeat(60);
    // manually computed: slice(0, 10) + '…' = 10 x's + ellipsis
    expect(truncateUrl(plain, 10)).toBe('x'.repeat(10) + '…');
  });

  it('returns empty string for empty string input without throwing', () => {
    // new URL('') throws TypeError; the catch block falls back to the raw string.
    // '' has length 0 which is ≤ any maxLen, so the empty string is returned as-is.
    expect(truncateUrl('', 50)).toBe('');
    expect(truncateUrl('')).toBe(''); // default maxLen path
  });

  // Intentional contract: truncateUrl drops the domain and returns path+search only.
  // TrafficAlerts/Index.tsx displays the returned value as a link label with the full
  // href still pointing at alert.page_url, so domain loss in the label is acceptable.
  it('returns path-only (no domain) for a realistic traffic alert URL — intentional design', () => {
    // Realistic URL from a TrafficAlert record
    const alertUrl = 'https://example.com/blog/my-seo-post?utm_source=gsc';
    // Manually computed expected: pathname '/blog/my-seo-post' + search '?utm_source=gsc'
    // = '/blog/my-seo-post?utm_source=gsc' (32 chars, well under maxLen 80)
    expect(truncateUrl(alertUrl, 80)).toBe('/blog/my-seo-post?utm_source=gsc');
  });

  // Intentional contract: hash fragments are stripped. truncateUrl returns
  // pathname + search only (via new URL().pathname + .search). GSC and traffic
  // alert URLs never carry meaningful fragments; the full href (including any
  // fragment) is preserved on the <a> element's href attribute.
  it('strips hash fragments from URLs — intentional design', () => {
    // URL with fragment: new URL parses hash into .hash, which we intentionally omit
    const url = 'https://example.com/page#section';
    // Manually computed: pathname = '/page', search = '', hash = '#section' (dropped)
    expect(truncateUrl(url, 100)).toBe('/page');
  });

  it('strips hash fragments even when query string is present', () => {
    const url = 'https://example.com/docs?v=2#heading-3';
    // Manually computed: pathname = '/docs', search = '?v=2', hash = '#heading-3' (dropped)
    expect(truncateUrl(url, 100)).toBe('/docs?v=2');
  });
});
