import { describe, expect, it } from 'vitest';
import {
  ONBOARDING_DAY_COUNT,
  orderThemes,
  distributeThemesIntoDays,
  computeTopicsFingerprint,
  computeOnboardingProgress,
  buildOnboardingPlan,
} from '../onboardingService';

const sizes = (days) => days.map((d) => d.themes.length);

describe('onboardingService pure helpers', () => {
  it('exposes a 5-day guideline', () => {
    expect(ONBOARDING_DAY_COUNT).toBe(5);
  });

  describe('orderThemes', () => {
    const kb = ['Governance', 'Culture', 'Privacy', 'Process'];

    it('orders by first appearance in the schedule, then appends the rest alphabetically', () => {
      const schedule = [{ theme: 'Privacy' }, { theme: 'Governance' }];
      expect(orderThemes(kb, schedule)).toEqual(['Privacy', 'Governance', 'Culture', 'Process']);
    });

    it('de-duplicates repeated schedule labels', () => {
      const schedule = [{ theme: 'Privacy' }, { theme: 'Privacy' }, { theme: 'Culture' }];
      expect(orderThemes(kb, schedule)).toEqual(['Privacy', 'Culture', 'Governance', 'Process']);
    });

    it('ignores schedule labels that are not real KB themes (e.g. merged-week labels)', () => {
      const schedule = [{ theme: 'Privacy & Governance (merged)' }, { theme: 'Culture' }];
      expect(orderThemes(kb, schedule)).toEqual(['Culture', 'Governance', 'Privacy', 'Process']);
    });

    it('falls back to pure alphabetical when the schedule is null/empty', () => {
      expect(orderThemes(kb, null)).toEqual(['Culture', 'Governance', 'Privacy', 'Process']);
      expect(orderThemes(kb, [])).toEqual(['Culture', 'Governance', 'Privacy', 'Process']);
    });
  });

  describe('distributeThemesIntoDays', () => {
    const themes = (n) => Array.from({ length: n }, (_, i) => `T${i + 1}`);

    it('splits 10 themes evenly across 5 days', () => {
      expect(sizes(distributeThemesIntoDays(themes(10)))).toEqual([2, 2, 2, 2, 2]);
    });

    it('front-loads the remainder (12 -> 3,3,2,2,2)', () => {
      expect(sizes(distributeThemesIntoDays(themes(12)))).toEqual([3, 3, 2, 2, 2]);
    });

    it('handles 7 -> 2,2,1,1,1', () => {
      expect(sizes(distributeThemesIntoDays(themes(7)))).toEqual([2, 2, 1, 1, 1]);
    });

    it('uses fewer days than the cap when there are fewer themes', () => {
      const days = distributeThemesIntoDays(themes(3));
      expect(sizes(days)).toEqual([1, 1, 1]);
      expect(days.map((d) => d.day)).toEqual([1, 2, 3]);
    });

    it('returns [] for no themes', () => {
      expect(distributeThemesIntoDays([])).toEqual([]);
    });

    it('preserves order and loses nothing', () => {
      const input = themes(12);
      const flat = distributeThemesIntoDays(input).flatMap((d) => d.themes);
      expect(flat).toEqual(input);
    });
  });

  describe('computeTopicsFingerprint', () => {
    it('is order-independent', () => {
      const a = computeTopicsFingerprint([{ id: 'x' }, { id: 'y' }, { id: 'z' }]);
      const b = computeTopicsFingerprint([{ id: 'z' }, { id: 'x' }, { id: 'y' }]);
      expect(a).toBe(b);
    });

    it('changes when a topic is added or removed', () => {
      const base = computeTopicsFingerprint([{ id: 'x' }, { id: 'y' }]);
      expect(computeTopicsFingerprint([{ id: 'x' }])).not.toBe(base);
      expect(computeTopicsFingerprint([{ id: 'x' }, { id: 'y' }, { id: 'z' }])).not.toBe(base);
    });

    it('handles empty input', () => {
      expect(typeof computeTopicsFingerprint([])).toBe('string');
    });
  });

  describe('computeOnboardingProgress', () => {
    const days = [
      { day: 1, themes: ['A', 'B'] },
      { day: 2, themes: ['C'] },
    ];

    it('does not count a partially-done day as complete', () => {
      const p = computeOnboardingProgress(days, new Set(['A']));
      expect(p).toMatchObject({ themesTotal: 3, themesDone: 1, dayCount: 2, daysCompleted: 0, allDone: false });
      expect(p.percentage).toBe(33);
    });

    it('counts a fully-done day', () => {
      const p = computeOnboardingProgress(days, new Set(['A', 'B']));
      expect(p).toMatchObject({ themesDone: 2, daysCompleted: 1, allDone: false });
    });

    it('reports allDone only when every theme is complete', () => {
      const p = computeOnboardingProgress(days, new Set(['A', 'B', 'C']));
      expect(p).toMatchObject({ themesDone: 3, daysCompleted: 2, percentage: 100, allDone: true });
    });

    it('accepts an array as well as a Set', () => {
      expect(computeOnboardingProgress(days, ['A', 'B', 'C']).allDone).toBe(true);
    });

    it('returns zeros for an empty plan', () => {
      expect(computeOnboardingProgress([], new Set())).toEqual({
        themesTotal: 0, themesDone: 0, dayCount: 0, daysCompleted: 0, percentage: 0, allDone: false,
      });
    });
  });

  describe('buildOnboardingPlan', () => {
    const topics = [
      { id: 't1', label: 'T1', theme: 'Privacy', complexity_weight: 2 },
      { id: 't2', label: 'T2', theme: 'Culture', complexity_weight: 1 },
      { id: 'f1', label: 'F1', theme: 'Privacy', type: 'fact' }, // excluded by buildThemeTopicMap
    ];

    it('builds themes (schedule-ordered), days, and a theme→topics map', () => {
      const activeVersion = { schedule: [{ theme: 'Privacy' }, { theme: 'Culture' }] };
      const plan = buildOnboardingPlan(topics, activeVersion);
      expect(plan.themes).toEqual(['Privacy', 'Culture']);
      expect(sizes(plan.days)).toEqual([1, 1]);
      expect(plan.themeTopicMap.get('Privacy').map((t) => t.id)).toEqual(['t1']); // fact excluded
    });

    it('tolerates a stringified schedule and a missing version', () => {
      const stringified = { schedule: JSON.stringify([{ theme: 'Culture' }]) };
      expect(buildOnboardingPlan(topics, stringified).themes).toEqual(['Culture', 'Privacy']);
      expect(buildOnboardingPlan(topics, null).themes).toEqual(['Culture', 'Privacy']);
    });

    it('returns an empty plan for an empty KB', () => {
      const plan = buildOnboardingPlan([], null);
      expect(plan.themes).toEqual([]);
      expect(plan.days).toEqual([]);
    });
  });
});
