import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readdirSync, readFileSync } from 'node:fs';
import path from 'node:path';

/**
 * Enforces the backend layer import direction (see
 * `backend/docs/backend-architecture-refactor-plan.md`):
 *
 *   Route -> Controller -> Service (BLL) -> Repository/Model (DAL) -> DB
 *   shared/constants/types/errors are cross-cutting and depend on no layer.
 *
 * Hard invariants assert zero violations. Debt ceilings cap known violations so
 * they cannot grow and are ratcheted down to 0 as the refactor phases land.
 */

const SRC = path.resolve(import.meta.dirname, '../..');

function listTs(dir: string): string[] {
  const out: string[] = [];
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
    const full = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      if (entry.name === 'node_modules') continue;
      out.push(...listTs(full));
    } else if (
      entry.name.endsWith('.ts') &&
      !entry.name.endsWith('.test.ts') &&
      !entry.name.endsWith('.d.ts')
    ) {
      out.push(full);
    }
  }
  return out;
}

const FILES = listTs(SRC).map((file) => ({
  rel: path.relative(SRC, file).split(path.sep).join('/'),
  imports: [...readFileSync(file, 'utf8').matchAll(/from '([^']+)'/g)].map(
    (m) => m[1],
  ),
}));

const API_LAYER = ['@/api/', '@/routes/', '@/middlewares/'];

function violators(layerPrefixes: string[], banned: string[]): string[] {
  return FILES.filter(
    (f) =>
      layerPrefixes.some((p) => f.rel.startsWith(p)) &&
      f.imports.some((i) => banned.some((b) => i === b || i.startsWith(b))),
  ).map((f) => f.rel);
}

// ---- Hard invariants (must be 0) ----

test('DAL models import no BLL, API layer, or Express', () => {
  assert.deepEqual(
    violators(['db/models/'], ['@/services/', 'express', ...API_LAYER]),
    [],
  );
});

test('cross-cutting code (shared/*) imports no layer', () => {
  assert.deepEqual(
    violators(
      ['shared/'],
      ['@/api/', '@/routes/', '@/middlewares/', '@/services/', '@/db/'],
    ),
    [],
  );
});

test('DAL does not import the API layer', () => {
  assert.deepEqual(violators(['db/'], API_LAYER), []);
});

// ---- Hard invariant earned in Phase 1-2 ----

test('API layer (routes, controllers) does not import the DAL', () => {
  assert.deepEqual(
    violators(['routes/', 'api/controllers/'], ['@/db/api', '@/db/models']),
    [],
  );
});

// ---- Debt ceilings (must not grow; lowered to 0 across phases) ----

test('BLL depends on HTTP only in the streaming/session edge cases', () => {
  // Remaining: `services/file.ts` (upload/download streaming) and
  // `services/auth.ts` (session IP/UA + cookies). To be revisited.
  const v = violators(['services/'], ['express', ...API_LAYER]);
  assert.ok(
    v.length <= 2,
    `services depending on HTTP grew to ${v.length} (>2: only file/auth remain)`,
  );
});

test('DAL depending on BLL — ceiling (→ 0)', () => {
  const v = violators(['db/api/'], ['@/services/']);
  assert.ok(
    v.length <= 1,
    `repositories importing services grew to ${v.length} (>1)`,
  );
});
