/**
 * graph.test.ts — Tests for BA order dependency graph and topological sort.
 */
import { describe, it, expect } from 'vitest';
import { buildBaOrderPlan } from '../graph.js';
import type { ModuleDep, ModuleKey, BaWave } from '../types.js';

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** Create a minimal ModuleDep for testing. */
function mod(
  app: string,
  module: string,
  deps: string[] = [],
): ModuleDep {
  return {
    appCode: app,
    moduleCode: module,
    key: `${app}/${module}` as ModuleKey,
    dependsOn: deps as ModuleKey[],
  };
}

/** Get internal (non-external) waves. */
function baWaves(waves: BaWave[]): BaWave[] {
  return waves.filter((w) => w.level > 0);
}

/** Get module keys from a wave. */
function waveKeys(wave: BaWave): string[] {
  return wave.modules.map((m) => m.key).sort();
}

// ---------------------------------------------------------------------------
// buildBaOrderPlan
// ---------------------------------------------------------------------------

describe('buildBaOrderPlan', () => {
  it('places all independent modules in Wave 1', () => {
    const modules = [
      mod('CRM', 'CONTACTS'),
      mod('CRM', 'SETTINGS'),
      mod('BILLING', 'PRODUCTS'),
    ];
    const plan = buildBaOrderPlan(modules, ['CRM', 'BILLING']);
    const waves = baWaves(plan.waves);

    expect(waves).toHaveLength(1);
    expect(waves[0].level).toBe(1);
    expect(waveKeys(waves[0])).toEqual([
      'BILLING/PRODUCTS',
      'CRM/CONTACTS',
      'CRM/SETTINGS',
    ]);
    expect(plan.cycles).toHaveLength(0);
    expect(plan.source).toBe('context');
  });

  it('chains dependent modules across waves', () => {
    const modules = [
      mod('ERP', 'REFERENCES'),
      mod('ERP', 'CUSTOMERS', ['ERP/REFERENCES']),
    ];
    const plan = buildBaOrderPlan(modules, ['ERP']);
    const waves = baWaves(plan.waves);

    expect(waves).toHaveLength(2);
    expect(waveKeys(waves[0])).toEqual(['ERP/REFERENCES']);
    expect(waveKeys(waves[1])).toEqual(['ERP/CUSTOMERS']);
  });

  it('handles diamond dependencies', () => {
    // BILLING depends on REFERENCES and CUSTOMERS; both are independent
    const modules = [
      mod('ERP', 'REFERENCES'),
      mod('ERP', 'CUSTOMERS'),
      mod('ERP', 'BILLING', ['ERP/REFERENCES', 'ERP/CUSTOMERS']),
    ];
    const plan = buildBaOrderPlan(modules, ['ERP']);
    const waves = baWaves(plan.waves);

    expect(waves).toHaveLength(2);
    expect(waveKeys(waves[0])).toEqual(['ERP/CUSTOMERS', 'ERP/REFERENCES']);
    expect(waveKeys(waves[1])).toEqual(['ERP/BILLING']);
  });

  it('handles a linear chain A → B → C', () => {
    const modules = [
      mod('A', 'REF'),
      mod('A', 'CLIENTS', ['A/REF']),
      mod('A', 'ORDERS', ['A/CLIENTS']),
    ];
    const plan = buildBaOrderPlan(modules, ['A']);
    const waves = baWaves(plan.waves);

    expect(waves).toHaveLength(3);
    expect(waveKeys(waves[0])).toEqual(['A/REF']);
    expect(waveKeys(waves[1])).toEqual(['A/CLIENTS']);
    expect(waveKeys(waves[2])).toEqual(['A/ORDERS']);
  });

  it('places external deps in Wave 0', () => {
    // INVOICES depends on CRM/CONTACTS, but CRM is not in scope
    const modules = [
      mod('BILLING', 'INVOICES', ['CRM/CONTACTS']),
    ];
    const plan = buildBaOrderPlan(modules, ['BILLING']);

    const wave0 = plan.waves.find((w) => w.level === 0);
    expect(wave0).toBeDefined();
    expect(wave0!.modules).toHaveLength(1);
    expect(wave0!.modules[0].key).toBe('CRM/CONTACTS');
    expect(wave0!.modules[0].isExternal).toBe(true);
    expect(wave0!.modules[0].referencedBy).toContain('BILLING/INVOICES');

    // INVOICES should be in Wave 1 (its only dep is external)
    const waves = baWaves(plan.waves);
    expect(waves).toHaveLength(1);
    expect(waveKeys(waves[0])).toEqual(['BILLING/INVOICES']);
  });

  it('detects and groups cycles in the same wave', () => {
    // A depends on B, B depends on A
    const modules = [
      mod('X', 'A', ['X/B']),
      mod('X', 'B', ['X/A']),
    ];
    const plan = buildBaOrderPlan(modules, ['X']);

    expect(plan.cycles).toHaveLength(1);
    expect(plan.cycles[0].sort()).toEqual(['X/A', 'X/B']);

    // Both should be in the same wave
    const waves = baWaves(plan.waves);
    expect(waves).toHaveLength(1);
    expect(waveKeys(waves[0])).toEqual(['X/A', 'X/B']);
  });

  it('handles cross-app dependencies within scope', () => {
    const modules = [
      mod('CRM', 'CONTACTS'),
      mod('BILLING', 'INVOICES', ['CRM/CONTACTS']),
    ];
    const plan = buildBaOrderPlan(modules, ['CRM', 'BILLING']);

    // No external deps
    expect(plan.waves.find((w) => w.level === 0)).toBeUndefined();

    const waves = baWaves(plan.waves);
    expect(waves).toHaveLength(2);
    expect(waveKeys(waves[0])).toEqual(['CRM/CONTACTS']);
    expect(waveKeys(waves[1])).toEqual(['BILLING/INVOICES']);
  });

  it('reports correct totalDeps', () => {
    const modules = [
      mod('CRM', 'CONTACTS'),
      mod('CRM', 'PIPELINE', ['CRM/CONTACTS']),
      mod('BILLING', 'INVOICES', ['CRM/CONTACTS', 'CRM/PIPELINE']),
    ];
    const plan = buildBaOrderPlan(modules, ['CRM', 'BILLING']);
    expect(plan.totalDeps).toBe(3);
  });

  it('handles empty module list', () => {
    const plan = buildBaOrderPlan([], ['CRM']);
    expect(plan.waves).toHaveLength(0);
    expect(plan.cycles).toHaveLength(0);
    expect(plan.totalModules).toBe(0);
  });
});
