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

import {
  classifyGscError,
  formatErrorForSupport,
  getAnalysisStepDescription,
  MAX_ERROR_LENGTH_FOR_MAILTO,
  SETUP_STEP_LABELS,
  SUPPORT_EMAIL,
  syncStageCopy,
} from './helpers';

describe('helpers/syncStageCopy', () => {
  it('returns the auth-stage copy for low progress', () => {
    expect(syncStageCopy(0)).toMatch(/Authenticating/);
    expect(syncStageCopy(15)).toMatch(/Authenticating/);
    expect(syncStageCopy(32)).toMatch(/Authenticating/);
  });

  it('transitions from auth → fetch at the 33% boundary', () => {
    // 32 is auth; 33 is fetch — boundary is exclusive on the upper side.
    expect(syncStageCopy(32)).toMatch(/Authenticating/);
    expect(syncStageCopy(33)).toMatch(/Fetching/);
  });

  it('returns the fetch-stage copy for mid-low progress', () => {
    expect(syncStageCopy(50)).toMatch(/Fetching/);
    expect(syncStageCopy(66)).toMatch(/Fetching/);
  });

  it('transitions from fetch → process at the 67% boundary', () => {
    expect(syncStageCopy(66)).toMatch(/Fetching/);
    expect(syncStageCopy(67)).toMatch(/Processing/);
  });

  it('returns the processing-stage copy for high progress', () => {
    expect(syncStageCopy(80)).toMatch(/Processing/);
    expect(syncStageCopy(89)).toMatch(/Processing/);
  });

  it('transitions from process → baseline at the 90% boundary', () => {
    expect(syncStageCopy(89)).toMatch(/Processing/);
    expect(syncStageCopy(90)).toMatch(/Building your performance baseline/);
  });

  it('returns the baseline-stage copy at and beyond 90%', () => {
    expect(syncStageCopy(90)).toMatch(/Building your performance baseline/);
    expect(syncStageCopy(99)).toMatch(/Building your performance baseline/);
    expect(syncStageCopy(100)).toMatch(/Building your performance baseline/);
  });

  it('treats out-of-range values defensively (callers are expected to clamp)', () => {
    // Negative values fall into the "< 33" branch.
    expect(syncStageCopy(-5)).toMatch(/Authenticating/);
    // Values above 100 fall into the "≥ 90" branch (final stage).
    expect(syncStageCopy(150)).toMatch(/Building your performance baseline/);
  });
});

describe('helpers/classifyGscError', () => {
  it('returns a generic retry recommendation for null input', () => {
    expect(classifyGscError(null)).toEqual({
      likelyCause: "We didn't get a specific reason from Google.",
      whatToDo: 'retry',
    });
  });

  it('returns a generic retry recommendation for undefined input', () => {
    expect(classifyGscError(undefined)).toEqual({
      likelyCause: "We didn't get a specific reason from Google.",
      whatToDo: 'retry',
    });
  });

  it('returns a generic retry recommendation for empty string', () => {
    expect(classifyGscError('')).toEqual({
      likelyCause: "We didn't get a specific reason from Google.",
      whatToDo: 'retry',
    });
  });

  it.each([
    'invalid_grant: token expired',
    'Token has been revoked.',
    '401 Unauthorized',
    'unauthorized_client',
  ])('classifies "%s" as a credentials problem (reconnect)', (errorMessage) => {
    const result = classifyGscError(errorMessage);
    expect(result.whatToDo).toBe('reconnect');
    expect(result.likelyCause).toMatch(/credentials/i);
  });

  it.each([
    'insufficient_permission for site',
    '403 Forbidden',
    'Insufficient scope: required scope is webmasters',
  ])('classifies "%s" as a permission problem (reconnect)', (errorMessage) => {
    const result = classifyGscError(errorMessage);
    expect(result.whatToDo).toBe('reconnect');
    expect(result.likelyCause).toMatch(/permission/i);
  });

  it.each([
    'Property not found in Search Console',
    '404 Not Found',
    'no_property: The requested property does not exist',
  ])('classifies "%s" as a missing-property problem (reconnect)', (errorMessage) => {
    const result = classifyGscError(errorMessage);
    expect(result.whatToDo).toBe('reconnect');
    expect(result.likelyCause).toMatch(/Search Console couldn't find/);
  });

  it.each(['quota exceeded', '429 Too Many Requests', 'Rate limit reached'])(
    'classifies "%s" as rate limiting (retry)',
    (errorMessage) => {
      const result = classifyGscError(errorMessage);
      expect(result.whatToDo).toBe('retry');
      expect(result.likelyCause).toMatch(/rate-limited/i);
    },
  );

  it.each(['Network error: connection refused', 'Connection timeout after 30s', 'ECONNRESET'])(
    'classifies "%s" as a transient network issue (retry)',
    (errorMessage) => {
      const result = classifyGscError(errorMessage);
      expect(result.whatToDo).toBe('retry');
      expect(result.likelyCause).toMatch(/network/i);
    },
  );

  it('falls through to contact_support for unrecognised errors', () => {
    const result = classifyGscError('Internal server error: weird things happened');
    expect(result.whatToDo).toBe('contact_support');
    expect(result.likelyCause).toMatch(/our side broke/i);
  });

  it('is case-insensitive', () => {
    // Same input in all-caps and all-lowercase should classify identically.
    const lower = classifyGscError('invalid_grant');
    const upper = classifyGscError('INVALID_GRANT');
    expect(lower).toEqual(upper);
  });
});

describe('helpers/formatErrorForSupport', () => {
  it('returns the placeholder for null', () => {
    expect(formatErrorForSupport(null)).toBe('(none reported)');
  });

  it('returns the placeholder for undefined', () => {
    expect(formatErrorForSupport(undefined)).toBe('(none reported)');
  });

  it('returns the placeholder for empty string', () => {
    expect(formatErrorForSupport('')).toBe('(none reported)');
  });

  it('returns short errors verbatim', () => {
    expect(formatErrorForSupport('invalid_grant')).toBe('invalid_grant');
  });

  it('truncates errors longer than the cap and marks them truncated', () => {
    const longError = 'x'.repeat(MAX_ERROR_LENGTH_FOR_MAILTO + 100);
    const formatted = formatErrorForSupport(longError);
    expect(formatted).toContain('… (truncated)');
    expect(formatted.length).toBeLessThan(longError.length);
  });

  it('preserves the cap exactly: an error of MAX_ERROR_LENGTH_FOR_MAILTO chars is not truncated', () => {
    const exactCapError = 'x'.repeat(MAX_ERROR_LENGTH_FOR_MAILTO);
    expect(formatErrorForSupport(exactCapError)).toBe(exactCapError);
  });
});

describe('helpers/getAnalysisStepDescription', () => {
  // R6UXA-012: persona branches collapsed to single solo path. All inputs
  // return the same time-anchored, action-oriented copy regardless of role.
  it('returns the solo persona copy for any role (including null/undefined)', () => {
    const expected = /pages worth fixing/;
    expect(getAnalysisStepDescription(null)).toMatch(expected);
    expect(getAnalysisStepDescription(undefined)).toMatch(expected);
    expect(getAnalysisStepDescription('not-a-real-role')).toMatch(expected);
    // Previously these had agency/freelancer-specific copy — now all resolve
    // to the single solo path (persona v1 alignment).
    expect(getAnalysisStepDescription('agency_owner')).toMatch(expected);
    expect(getAnalysisStepDescription('blogger')).toMatch(expected);
    expect(getAnalysisStepDescription('ecommerce_manager')).toMatch(expected);
    expect(getAnalysisStepDescription('freelancer')).toMatch(expected);
    expect(getAnalysisStepDescription('in_house_seo')).toMatch(expected);
  });
});

describe('helpers/SUPPORT_EMAIL', () => {
  it('uses the .ai (not .app) domain to match DashboardLayout', () => {
    // Guard against the typo I introduced in an earlier pass: rankwiz.app is
    // not the canonical support inbox.
    expect(SUPPORT_EMAIL).toBe('support@rankwiz.ai');
  });
});

describe('helpers/SETUP_STEP_LABELS', () => {
  // R6UXA-013: guard against key renames or value drift — both the onboarding wizard
  // and the Billing trial-recap modal consume this constant; they must stay in sync.
  it('has the three expected keys matching SetupState boolean flags', () => {
    expect(Object.keys(SETUP_STEP_LABELS)).toEqual(
      expect.arrayContaining(['has_gsc', 'has_analysis', 'has_ai_key']),
    );
    expect(Object.keys(SETUP_STEP_LABELS)).toHaveLength(3);
  });

  it('has non-empty string values for each key', () => {
    expect(SETUP_STEP_LABELS.has_gsc).toBeTruthy();
    expect(SETUP_STEP_LABELS.has_analysis).toBeTruthy();
    expect(SETUP_STEP_LABELS.has_ai_key).toBeTruthy();
  });

  it('contains expected copy for GSC step', () => {
    expect(SETUP_STEP_LABELS.has_gsc).toMatch(/Google Search Console/i);
  });
});
