import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readdirSync, readFileSync } from 'node:fs';
import { dirname, join, relative, sep } from 'node:path';
import { dirname as projectDirname, normalize as normalizeProjectPath } from 'node:path/posix';
import { fileURLToPath } from 'node:url';

type Layer = 'api' | 'business' | 'data' | 'shared' | 'other';

interface SourceFile {
  readonly absolutePath: string;
  readonly projectPath: string;
  readonly layer: Layer;
  readonly source: string;
}

interface ImportReference {
  readonly specifier: string;
  readonly projectPath: string | null;
}

const sourceRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const sourceFileExtensions = ['.ts'];
const importPattern =
  /(?:import|export)\s+(?:type\s+)?(?:[\s\S]*?\sfrom\s+)?['"]([^'"]+)['"]|import\(\s*['"]([^'"]+)['"]\s*\)/g;

const allowedApiToDataImports = [
  'auth/auth.ts -> @/db/api/users',
  'middlewares/check-permissions.ts -> @/db/api/roles',
];

const allowedBusinessHttpImports = [
  'services/auth.ts -> express',
  'services/file.ts -> @/middlewares/upload',
  'services/file.ts -> express',
];

const allowedDataToBusinessImports = [
  'db/api/file.ts -> @/services/file',
];

function toProjectPath(absolutePath: string): string {
  return relative(sourceRoot, absolutePath).split(sep).join('/');
}

function isSourceFile(fileName: string): boolean {
  return sourceFileExtensions.some((extension) => fileName.endsWith(extension));
}

function isTestFile(projectPath: string): boolean {
  return projectPath.endsWith('.test.ts') || projectPath.endsWith('.d.ts');
}

function getLayer(projectPath: string): Layer {
  if (
    projectPath === 'index.ts'
    || projectPath.startsWith('commands/')
    || projectPath.startsWith('api/')
    || projectPath.startsWith('auth/')
    || projectPath.startsWith('middlewares/')
    || projectPath.startsWith('routes/')
  ) {
    return 'api';
  }

  if (projectPath.startsWith('services/')) {
    return 'business';
  }

  if (projectPath.startsWith('db/')) {
    return 'data';
  }

  if (projectPath.startsWith('shared/') || projectPath.startsWith('types/')) {
    return 'shared';
  }

  return 'other';
}

function listSourceFiles(directory: string): readonly SourceFile[] {
  return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
    const absolutePath = join(directory, entry.name);

    if (entry.isDirectory()) {
      return listSourceFiles(absolutePath);
    }

    if (!entry.isFile() || !isSourceFile(entry.name)) {
      return [];
    }

    const projectPath = toProjectPath(absolutePath);

    if (isTestFile(projectPath) || projectPath.startsWith('test-utils/')) {
      return [];
    }

    return [
      {
        absolutePath,
        projectPath,
        layer: getLayer(projectPath),
        source: readFileSync(absolutePath, 'utf8'),
      },
    ];
  });
}

function resolveProjectImport(importerProjectPath: string, specifier: string): string | null {
  if (specifier.startsWith('@/')) {
    return specifier.slice(2);
  }

  if (!specifier.startsWith('.')) {
    return null;
  }

  return normalizeProjectPath(`${projectDirname(importerProjectPath)}/${specifier}`);
}

function readImports(sourceFile: SourceFile): readonly ImportReference[] {
  const imports: ImportReference[] = [];
  const matches = sourceFile.source.matchAll(importPattern);

  for (const match of matches) {
    const specifier = match[1] ?? match[2];

    if (specifier === undefined) {
      continue;
    }

    imports.push({
      specifier,
      projectPath: resolveProjectImport(sourceFile.projectPath, specifier),
    });
  }

  return imports;
}

function formatViolation(sourceFile: SourceFile, importReference: ImportReference): string {
  return `${sourceFile.projectPath} -> ${importReference.specifier}`;
}

function collectViolations(
  sourceFiles: readonly SourceFile[],
  isSourceInScope: (sourceFile: SourceFile) => boolean,
  isForbiddenImport: (sourceFile: SourceFile, importReference: ImportReference) => boolean,
): readonly string[] {
  return sourceFiles
    .filter(isSourceInScope)
    .flatMap((sourceFile) =>
      readImports(sourceFile)
        .filter((importReference) => isForbiddenImport(sourceFile, importReference))
        .map((importReference) => formatViolation(sourceFile, importReference)),
    )
    .sort();
}

function withoutAllowed(violations: readonly string[], allowed: readonly string[]): readonly string[] {
  const allowedSet = new Set(allowed);
  return violations.filter((violation) => !allowedSet.has(violation));
}

function assertOnlyAllowed(violations: readonly string[], allowed: readonly string[]): void {
  assert.deepEqual(withoutAllowed(violations, allowed), []);
  assert.deepEqual(
    violations.filter((violation) => allowed.includes(violation)).sort(),
    [...allowed].sort(),
  );
}

function isProjectLayer(importReference: ImportReference, layer: Layer): boolean {
  return importReference.projectPath !== null && getLayer(importReference.projectPath) === layer;
}

function isHttpImport(importReference: ImportReference): boolean {
  return importReference.specifier === 'express'
    || importReference.specifier.startsWith('@/api/')
    || importReference.specifier.startsWith('@/auth/')
    || importReference.specifier.startsWith('@/middlewares/')
    || importReference.specifier.startsWith('@/routes/');
}

test('API layer does not import data directly beyond explicit edge wiring', () => {
  const violations = collectViolations(
    listSourceFiles(sourceRoot),
    (sourceFile) => sourceFile.layer === 'api',
    (_sourceFile, importReference) => isProjectLayer(importReference, 'data'),
  );

  assertOnlyAllowed(violations, allowedApiToDataImports);
});

test('all production source files are assigned to an architecture layer', () => {
  const unclassifiedFiles = listSourceFiles(sourceRoot)
    .filter((sourceFile) => sourceFile.layer === 'other')
    .map((sourceFile) => sourceFile.projectPath)
    .sort();

  assert.deepEqual(unclassifiedFiles, []);
});

test('routes stay thin: no direct business or data imports', () => {
  const violations = collectViolations(
    listSourceFiles(sourceRoot),
    (sourceFile) => sourceFile.projectPath.startsWith('routes/'),
    (_sourceFile, importReference) =>
      isProjectLayer(importReference, 'business') || isProjectLayer(importReference, 'data'),
  );

  assert.deepEqual(violations, []);
});

test('controllers do not import data repositories or models directly', () => {
  const violations = collectViolations(
    listSourceFiles(sourceRoot),
    (sourceFile) => sourceFile.projectPath.startsWith('api/controllers/'),
    (_sourceFile, importReference) => isProjectLayer(importReference, 'data'),
  );

  assert.deepEqual(violations, []);
});

test('business layer keeps HTTP dependencies limited to known edge cases', () => {
  const violations = collectViolations(
    listSourceFiles(sourceRoot),
    (sourceFile) => sourceFile.layer === 'business',
    (_sourceFile, importReference) => isHttpImport(importReference),
  );

  assertOnlyAllowed(violations, allowedBusinessHttpImports);
});

test('data layer does not import API, business, or Express beyond explicit file storage bridge', () => {
  const violations = collectViolations(
    listSourceFiles(sourceRoot),
    (sourceFile) => sourceFile.layer === 'data',
    (_sourceFile, importReference) =>
      isProjectLayer(importReference, 'api')
      || isProjectLayer(importReference, 'business')
      || importReference.specifier === 'express',
  );

  assertOnlyAllowed(violations, allowedDataToBusinessImports);
});

test('model layer imports no business, API, or Express modules', () => {
  const violations = collectViolations(
    listSourceFiles(sourceRoot),
    (sourceFile) => sourceFile.projectPath.startsWith('db/models/'),
    (_sourceFile, importReference) =>
      isProjectLayer(importReference, 'api')
      || isProjectLayer(importReference, 'business')
      || importReference.specifier === 'express',
  );

  assert.deepEqual(violations, []);
});

test('shared code stays cross-cutting and imports no application layer', () => {
  const violations = collectViolations(
    listSourceFiles(sourceRoot),
    (sourceFile) => sourceFile.layer === 'shared',
    (_sourceFile, importReference) =>
      isProjectLayer(importReference, 'api')
      || isProjectLayer(importReference, 'business')
      || isProjectLayer(importReference, 'data')
      || importReference.specifier === 'express',
  );

  assert.deepEqual(violations, []);
});
