/**
 * graph.test.ts — Tests for dependency graph, cycle detection, and topological sort.
 */
import { describe, it, expect } from 'vitest';
import { buildPlan, tarjanSCC } from '../graph.js';
import type { ModuleInfo, ModuleKey, Wave } from '../types.js';

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

/** Create a minimal ModuleInfo for testing. */
function mod(
  app: string,
  module: string,
  deps: Array<{ targetApp: string; targetModule: string }> = [],
  entityCount = 1,
): ModuleInfo {
  return {
    appCode: app,
    moduleCode: module,
    key: `${app}/${module}` as ModuleKey,
    entities: Array.from({ length: entityCount }, (_, i) => ({
      code: `ENT-${String(i + 1).padStart(3, '0')}`,
      name: `Entity${i + 1}`,
    })),
    crossModuleDeps: deps.map((d) => ({
      sourceEntity: 'SomeEntity',
      sourceAttribute: `${d.targetModule}Id`,
      targetApp: d.targetApp,
      targetModule: d.targetModule,
      targetEntity: 'SomeTarget',
    })),
    coreRefs: [],
    hasEntiteMd: true,
  };
}

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

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

// ---------------------------------------------------------------------------
// Tarjan SCC
// ---------------------------------------------------------------------------

describe('tarjanSCC', () => {
  it('finds no cycles in a DAG', () => {
    const nodes: ModuleKey[] = ['A/X' as ModuleKey, 'A/Y' as ModuleKey];
    const adj = new Map<ModuleKey, Set<ModuleKey>>([
      ['A/X' as ModuleKey, new Set(['A/Y' as ModuleKey])],
      ['A/Y' as ModuleKey, new Set()],
    ]);
    const cycles = tarjanSCC(nodes, adj);
    expect(cycles).toHaveLength(0);
  });

  it('finds a 2-node cycle', () => {
    const nodes: ModuleKey[] = ['A/X' as ModuleKey, 'A/Y' as ModuleKey];
    const adj = new Map<ModuleKey, Set<ModuleKey>>([
      ['A/X' as ModuleKey, new Set(['A/Y' as ModuleKey])],
      ['A/Y' as ModuleKey, new Set(['A/X' as ModuleKey])],
    ]);
    const cycles = tarjanSCC(nodes, adj);
    expect(cycles).toHaveLength(1);
    expect(cycles[0].sort()).toEqual(['A/X', 'A/Y']);
  });

  it('finds a 3-node cycle', () => {
    const nodes: ModuleKey[] = [
      'A/X' as ModuleKey,
      'A/Y' as ModuleKey,
      'A/Z' as ModuleKey,
    ];
    const adj = new Map<ModuleKey, Set<ModuleKey>>([
      ['A/X' as ModuleKey, new Set(['A/Y' as ModuleKey])],
      ['A/Y' as ModuleKey, new Set(['A/Z' as ModuleKey])],
      ['A/Z' as ModuleKey, new Set(['A/X' as ModuleKey])],
    ]);
    const cycles = tarjanSCC(nodes, adj);
    expect(cycles).toHaveLength(1);
    expect(cycles[0].sort()).toEqual(['A/X', 'A/Y', 'A/Z']);
  });
});

// ---------------------------------------------------------------------------
// buildPlan — topological sort into waves
// ---------------------------------------------------------------------------

describe('buildPlan', () => {
  it('places all independent modules in Wave 1', () => {
    const modules = [
      mod('CRM', 'CONTACTS'),
      mod('CRM', 'SETTINGS'),
      mod('BILLING', 'PRODUCTS'),
    ];
    const plan = buildPlan(modules, ['CRM', 'BILLING']);
    const waves = devWaves(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);
  });

  it('chains dependent modules across waves', () => {
    // PIPELINE depends on CONTACTS
    const modules = [
      mod('CRM', 'CONTACTS'),
      mod('CRM', 'PIPELINE', [{ targetApp: 'CRM', targetModule: 'CONTACTS' }]),
    ];
    const plan = buildPlan(modules, ['CRM']);
    const waves = devWaves(plan.waves);

    expect(waves).toHaveLength(2);
    expect(waveKeys(waves[0])).toEqual(['CRM/CONTACTS']);
    expect(waveKeys(waves[1])).toEqual(['CRM/PIPELINE']);
  });

  it('handles diamond dependencies', () => {
    // D depends on B and C; B and C are independent
    const modules = [
      mod('A', 'B'),
      mod('A', 'C'),
      mod('A', 'D', [
        { targetApp: 'A', targetModule: 'B' },
        { targetApp: 'A', targetModule: 'C' },
      ]),
    ];
    const plan = buildPlan(modules, ['A']);
    const waves = devWaves(plan.waves);

    expect(waves).toHaveLength(2);
    expect(waveKeys(waves[0])).toEqual(['A/B', 'A/C']);
    expect(waveKeys(waves[1])).toEqual(['A/D']);
  });

  it('handles a linear chain A → B → C', () => {
    const modules = [
      mod('A', 'C'),
      mod('A', 'B', [{ targetApp: 'A', targetModule: 'C' }]),
      mod('A', 'A', [{ targetApp: 'A', targetModule: 'B' }]),
    ];
    const plan = buildPlan(modules, ['A']);
    const waves = devWaves(plan.waves);

    expect(waves).toHaveLength(3);
    expect(waveKeys(waves[0])).toEqual(['A/C']);
    expect(waveKeys(waves[1])).toEqual(['A/B']);
    expect(waveKeys(waves[2])).toEqual(['A/A']);
  });

  it('places external deps in Wave 0', () => {
    // INVOICES depends on CRM/CONTACTS, but CRM is not in scope
    const modules = [
      mod('BILLING', 'INVOICES', [
        { targetApp: 'CRM', targetModule: 'CONTACTS' },
      ]),
    ];
    const plan = buildPlan(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 = devWaves(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', [{ targetApp: 'X', targetModule: 'B' }]),
      mod('X', 'B', [{ targetApp: 'X', targetModule: 'A' }]),
    ];
    const plan = buildPlan(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 = devWaves(plan.waves);
    expect(waves).toHaveLength(1);
    expect(waveKeys(waves[0])).toEqual(['X/A', 'X/B']);
  });

  it('handles cycle plus non-cycle modules', () => {
    // C depends on cycle (A↔B), D is independent
    const modules = [
      mod('X', 'A', [{ targetApp: 'X', targetModule: 'B' }]),
      mod('X', 'B', [{ targetApp: 'X', targetModule: 'A' }]),
      mod('X', 'C', [{ targetApp: 'X', targetModule: 'A' }]),
      mod('X', 'D'),
    ];
    const plan = buildPlan(modules, ['X']);

    expect(plan.cycles).toHaveLength(1);

    const waves = devWaves(plan.waves);
    // D and A↔B have no external deps → could be Wave 1
    // C depends on A → Wave 2
    // A↔B are collapsed into one group at Wave 1

    // Find which wave has D and cycle group
    const wave1Keys = waveKeys(waves[0]);
    expect(wave1Keys).toContain('X/D');
    expect(wave1Keys).toContain('X/A');
    expect(wave1Keys).toContain('X/B');

    // C should be in Wave 2
    const wave2 = waves.find((w) => waveKeys(w).includes('X/C'));
    expect(wave2).toBeDefined();
    expect(wave2!.level).toBeGreaterThan(waves[0].level);
  });

  it('handles cross-app dependencies within scope', () => {
    // BILLING/INVOICES depends on CRM/CONTACTS, both in scope
    const modules = [
      mod('CRM', 'CONTACTS'),
      mod('BILLING', 'INVOICES', [
        { targetApp: 'CRM', targetModule: 'CONTACTS' },
      ]),
    ];
    const plan = buildPlan(modules, ['CRM', 'BILLING']);

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

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

  it('reports correct totalCrossModuleDeps', () => {
    const modules = [
      mod('CRM', 'CONTACTS'),
      mod('CRM', 'PIPELINE', [
        { targetApp: 'CRM', targetModule: 'CONTACTS' },
      ]),
      mod('BILLING', 'INVOICES', [
        { targetApp: 'CRM', targetModule: 'CONTACTS' },
        { targetApp: 'CRM', targetModule: 'PIPELINE' },
      ]),
    ];
    const plan = buildPlan(modules, ['CRM', 'BILLING']);
    expect(plan.totalCrossModuleDeps).toBe(3);
  });

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