import { describe, expect, it } from 'vitest';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const currentDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(currentDir, '../../..');
const migrationDir = path.resolve(currentDir, '../../src/infra/database/migrations');
const migrationPath = path.resolve(migrationDir, '044_prefer-request-jwt-claims.sql');

const historicalDottedClaimMigrations = new Set([
  'backend/src/infra/database/migrations/001_create-helper-functions.sql',
  'backend/src/infra/database/migrations/013_create-auth-schema-functions.sql',
]);

function toPosixRelative(filePath: string): string {
  return path.relative(repoRoot, filePath).split(path.sep).join('/');
}

function collectFiles(dir: string, predicate: (filePath: string) => boolean): string[] {
  const ignoredDirs = new Set(['.git', 'node_modules', 'dist', 'coverage']);
  const entries = fs.readdirSync(dir, { withFileTypes: true });
  const files: string[] = [];

  for (const entry of entries) {
    const entryPath = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      if (!ignoredDirs.has(entry.name)) {
        files.push(...collectFiles(entryPath, predicate));
      }
      continue;
    }

    if (entry.isFile() && predicate(entryPath)) {
      files.push(entryPath);
    }
  }

  return files;
}

function readRepoFiles(
  paths: string[],
  extensions: string[]
): Array<{ path: string; content: string }> {
  return paths.flatMap((relativePath) => {
    const absolutePath = path.resolve(repoRoot, relativePath);
    if (!fs.existsSync(absolutePath)) {
      return [];
    }

    return collectFiles(absolutePath, (filePath) =>
      extensions.some((extension) => filePath.endsWith(extension))
    ).map((filePath) => ({
      path: toPosixRelative(filePath),
      content: fs.readFileSync(filePath, 'utf8'),
    }));
  });
}

function readSql(): string {
  return fs.readFileSync(migrationPath, 'utf8');
}

describe('044_prefer-request-jwt-claims migration', () => {
  it('migration file exists', () => {
    expect(fs.existsSync(migrationPath)).toBe(true);
  });

  it.each(['uid', 'role', 'email'])('redefines auth.%s with CREATE OR REPLACE', (fnName) => {
    const sql = readSql();
    expect(sql).toMatch(new RegExp(`CREATE OR REPLACE FUNCTION auth\\.${fnName}\\(\\)`, 'i'));
  });

  it.each([
    ['uid', 'sub'],
    ['role', 'role'],
    ['email', 'email'],
  ])('auth.%s reads only canonical auth.jwt() claim %s', (_fnName, claimName) => {
    const sql = readSql();
    expect(sql).toMatch(
      new RegExp(`SELECT nullif\\(auth\\.jwt\\(\\)\\s*->>\\s*'${claimName}',\\s*''\\)::`, 'i')
    );
  });

  it('does not keep legacy dotted claim fallback', () => {
    const sql = readSql();
    expect(sql).not.toMatch(/request\.jwt\.claim\./i);
  });

  it('does not add or replace public helper functions', () => {
    const sql = readSql();
    expect(sql).not.toMatch(/CREATE OR REPLACE FUNCTION public\./i);
  });

  it('grants only missing storage runtime roles required before RLS can evaluate', () => {
    const sql = readSql();
    expect(sql).toMatch(/GRANT\s+USAGE\s+ON\s+SCHEMA\s+storage\s+TO\s+anon,\s*project_admin/i);
    expect(sql).toMatch(
      /GRANT\s+SELECT,\s*INSERT,\s*UPDATE,\s*DELETE\s+ON\s+storage\.objects\s+TO\s+anon,\s*project_admin/i
    );
    expect(sql).not.toMatch(/GRANT\s+SELECT\s+ON\s+storage\.buckets/i);
  });

  it('runs after migration 043', () => {
    const migrations = fs
      .readdirSync(migrationDir)
      .filter((file) => file.endsWith('.sql'))
      .sort();

    expect(migrations.indexOf('044_prefer-request-jwt-claims.sql')).toBeGreaterThan(
      migrations.indexOf('043_drop-deprecated-ai-configs-and-usage.sql')
    );
  });

  it('keeps dotted jwt claims confined to immutable historical migrations', () => {
    const scannedFiles = readRepoFiles(
      [
        'backend/src',
        'backend/tests',
        'docs',
        'packages',
        'openapi',
        '.agents',
        '.codex',
        '.claude',
      ],
      ['.ts', '.tsx', '.js', '.jsx', '.sql', '.md', '.mdx', '.sh', '.json']
    );

    const offenders = scannedFiles
      .filter(({ content }) => /request\.jwt\.claim\./i.test(content))
      .map(({ path: filePath }) => filePath)
      .filter((filePath) => !historicalDottedClaimMigrations.has(filePath));

    expect(offenders).toEqual([]);
  });

  it('does not reference the removed db user-context service path', () => {
    const scannedFiles = readRepoFiles(
      ['backend/src', 'backend/tests', '.agents', '.codex', '.claude'],
      ['.ts', '.tsx', '.js', '.jsx', '.md', '.mdx']
    );

    const offenders = scannedFiles
      .filter(({ content }) => /services\/db\/user-context/i.test(content))
      .map(({ path: filePath }) => filePath);

    expect(offenders).toEqual([]);
  });

  it('does not reference the removed admin/database context abstraction', () => {
    const scannedFiles = readRepoFiles(
      [
        'backend/src',
        'backend/tests',
        'docs',
        'packages',
        'openapi',
        '.agents',
        '.codex',
        '.claude',
      ],
      ['.ts', '.tsx', '.js', '.jsx', '.sql', '.md', '.mdx', '.sh', '.json']
    );
    const removedContextPattern = new RegExp(
      [
        ['ADMIN', 'DATABASE', 'CONTEXT'].join('_'),
        ['Database', 'Context'].join(''),
        ['with', 'Database', 'Context'].join(''),
      ].join('|'),
      'i'
    );

    const offenders = scannedFiles
      .filter(({ content }) => removedContextPattern.test(content))
      .map(({ path: filePath }) => filePath);

    expect(offenders).toEqual([]);
  });
});
