import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { FrameworkScanner } from '../scanners/framework-scanner';
import { EnvScanner } from '../scanners/env-scanner';
import { AstSecurityScanner } from '../scanners/ast-security-scanner';
import { VibeScanner } from '../scanners/vibe-scanner';
import { resolvesViaTsconfigPaths } from '../utils/tsconfig-resolver';
import { UbonScan } from '../index';

describe('3.0.2 quick-wins — false-positive killers, noise reduction, new rules', () => {
  let dir: string;

  beforeEach(() => {
    dir = mkdtempSync(join(tmpdir(), 'ubon-302-'));
    writeFileSync(
      join(dir, 'package.json'),
      JSON.stringify({ name: 't', dependencies: { next: '15.0.0' } })
    );
  });

  afterEach(() => {
    rmSync(dir, { recursive: true, force: true });
  });

  const writeFile = (rel: string, content: string) => {
    const full = join(dir, rel);
    mkdirSync(join(full, '..'), { recursive: true });
    writeFileSync(full, content);
  };

  // ---- Tier 1 ----------------------------------------------------------

  describe('tsconfig paths resolver', () => {
    it('resolves @/lib/db via tsconfig paths (VIBE001 false positive killer)', () => {
      writeFile(
        'tsconfig.json',
        JSON.stringify({ compilerOptions: { paths: { '@/*': ['./*'] } } })
      );
      writeFile('lib/db.ts', 'export const db = {};');
      expect(resolvesViaTsconfigPaths('@/lib/db', dir)).toBe(true);
      expect(resolvesViaTsconfigPaths('@/lib/ghost', dir)).toBe(false);
    });

    it('resolves via baseUrl fallback', () => {
      writeFile(
        'tsconfig.json',
        JSON.stringify({ compilerOptions: { baseUrl: './src' } })
      );
      writeFile('src/components/Foo.tsx', 'export const Foo = () => null;');
      expect(resolvesViaTsconfigPaths('components/Foo', dir)).toBe(true);
    });

    it('suppresses VIBE001 when the alias resolves on disk', async () => {
      writeFile(
        'tsconfig.json',
        JSON.stringify({ compilerOptions: { paths: { '@/*': ['./*'] } } })
      );
      writeFile('package.json', JSON.stringify({ name: 'x', dependencies: {} }));
      writeFile('lib/db.ts', 'export const db = {};');
      writeFile('app/page.tsx', "import { db } from '@/lib/db';\n");
      const scanner = new VibeScanner();
      const results = await scanner.scan({ directory: dir, verbose: false } as any);
      expect(results.some((r) => r.ruleId === 'VIBE001')).toBe(false);
    });
  });

  describe('orchestrator-level dedup', () => {
    it('collapses duplicate (ruleId, file, line) findings', async () => {
      const raw = [
        { ruleId: 'SEC017', file: 'a.tsx', line: 12, confidence: 0.7, category: 'security', severity: 'medium', type: 'warning', message: 'x' },
        { ruleId: 'SEC017', file: 'a.tsx', line: 12, confidence: 0.9, category: 'security', severity: 'medium', type: 'warning', message: 'x (better)' },
        { ruleId: 'SEC017', file: 'a.tsx', line: 14, confidence: 0.8, category: 'security', severity: 'medium', type: 'warning', message: 'x' }
      ] as any;
      const scan = new UbonScan();
      const deduped = (scan as any).dedupeResults(raw);
      expect(deduped).toHaveLength(2);
      const l12 = deduped.find((r: any) => r.line === 12);
      expect(l12.confidence).toBe(0.9);
      expect(l12.message).toBe('x (better)');
    });
  });

  // ---- Tier 3 ----------------------------------------------------------

  describe('ENV008 — NEXT_PUBLIC_ connection-URL leak', () => {
    it('flags NEXT_PUBLIC_DATABASE_URL with postgres://', async () => {
      writeFile('.env.local', 'NEXT_PUBLIC_DATABASE_URL=postgres://u:p@h/db\n');
      writeFile('.gitignore', '.env*\n');
      const scanner = new EnvScanner();
      const results = await scanner.scan({ directory: dir, verbose: false } as any);
      expect(results.some((r) => r.ruleId === 'ENV008')).toBe(true);
    });

    it('does not flag a server-side DATABASE_URL', async () => {
      writeFile('.env.local', 'DATABASE_URL=postgres://u:p@h/db\n');
      writeFile('.gitignore', '.env*\n');
      const scanner = new EnvScanner();
      const results = await scanner.scan({ directory: dir, verbose: false } as any);
      expect(results.some((r) => r.ruleId === 'ENV008')).toBe(false);
    });

    it('also flags VITE_/PUBLIC_/EXPO_PUBLIC_ prefixes', async () => {
      writeFile(
        '.env.local',
        [
          'VITE_DATABASE_URL=mongodb://u:p@h/db',
          'PUBLIC_REDIS_URL=redis://u:p@h/0',
          'EXPO_PUBLIC_API_ENDPOINT=mysql://u:p@h/db'
        ].join('\n') + '\n'
      );
      writeFile('.gitignore', '.env*\n');
      const scanner = new EnvScanner();
      const results = await scanner.scan({ directory: dir, verbose: false } as any);
      expect(results.filter((r) => r.ruleId === 'ENV008').length).toBe(3);
    });
  });

  describe('SEC020 — SQL sink with string interpolation', () => {
    it('flags db.prepare(`... ${x} ...`)', async () => {
      writeFile('package.json', JSON.stringify({ name: 'x' }));
      writeFile(
        'app/api/notes/route.ts',
        "const rows = db.prepare(`SELECT * FROM notes WHERE title LIKE '%${q}%'`).all();\n"
      );
      const scanner = new AstSecurityScanner();
      const results = await scanner.scan({ directory: dir, verbose: false } as any);
      expect(results.some((r: any) => r.ruleId === 'SEC020')).toBe(true);
    });

    it('flags $queryRawUnsafe("... " + x)', async () => {
      writeFile('package.json', JSON.stringify({ name: 'x' }));
      writeFile(
        'lib/db.ts',
        "prisma.$queryRawUnsafe('SELECT * FROM u WHERE id = ' + userId);\n"
      );
      const scanner = new AstSecurityScanner();
      const results = await scanner.scan({ directory: dir, verbose: false } as any);
      expect(results.some((r: any) => r.ruleId === 'SEC020')).toBe(true);
    });

    it('does not flag a clean parameterised .prepare("... ?")', async () => {
      writeFile('package.json', JSON.stringify({ name: 'x' }));
      writeFile(
        'lib/db.ts',
        "const stmt = db.prepare('SELECT * FROM notes WHERE id = ?');\nstmt.get(id);\n"
      );
      const scanner = new AstSecurityScanner();
      const results = await scanner.scan({ directory: dir, verbose: false } as any);
      expect(results.some((r) => r.ruleId === 'SEC020')).toBe(false);
    });
  });

  describe('NEXT216 — Next 15 async params/searchParams', () => {
    it('flags `params: { id: string }` in app/notes/[id]/page.tsx', async () => {
      writeFile(
        'app/notes/[id]/page.tsx',
        "export default function NotePage({ params }: { params: { id: string } }) { return null; }\n"
      );
      const scanner = new FrameworkScanner();
      const results = await scanner.scan({ directory: dir, verbose: false } as any);
      expect(results.some((r) => r.ruleId === 'NEXT216')).toBe(true);
    });

    it('does NOT flag `params: Promise<{ id: string }>`', async () => {
      writeFile(
        'app/notes/[id]/page.tsx',
        "export default async function NotePage({ params }: { params: Promise<{ id: string }> }) { await params; return null; }\n"
      );
      const scanner = new FrameworkScanner();
      const results = await scanner.scan({ directory: dir, verbose: false } as any);
      expect(results.some((r) => r.ruleId === 'NEXT216')).toBe(false);
    });
  });

  describe('NEXT217 — hook usage without `use client`', () => {
    it('flags useState import in a .tsx with no directive', async () => {
      writeFile(
        'components/NoteList.tsx',
        "import { useState, useEffect } from 'react';\nexport function NoteList() { const [x, setX] = useState(0); return null; }\n"
      );
      const scanner = new FrameworkScanner();
      const results = await scanner.scan({ directory: dir, verbose: false } as any);
      expect(results.some((r) => r.ruleId === 'NEXT217')).toBe(true);
    });

    it('does NOT flag the same file with `use client`', async () => {
      writeFile(
        'components/NoteList.tsx',
        "'use client';\nimport { useState } from 'react';\nexport function NoteList() { const [x, setX] = useState(0); return null; }\n"
      );
      const scanner = new FrameworkScanner();
      const results = await scanner.scan({ directory: dir, verbose: false } as any);
      expect(results.some((r) => r.ruleId === 'NEXT217')).toBe(false);
    });

    it('does NOT flag a file that only imports non-hook react exports', async () => {
      writeFile(
        'components/Foo.tsx',
        "import { Fragment } from 'react';\nexport const Foo = () => <Fragment />;\n"
      );
      const scanner = new FrameworkScanner();
      const results = await scanner.scan({ directory: dir, verbose: false } as any);
      expect(results.some((r) => r.ruleId === 'NEXT217')).toBe(false);
    });
  });

  describe('NEXT218 / NEXT219 — next.config hints', () => {
    it('flags `reactStrictMode: false`', async () => {
      writeFile(
        'next.config.js',
        "module.exports = { reactStrictMode: false };\n"
      );
      const scanner = new FrameworkScanner();
      const results = await scanner.scan({ directory: dir, verbose: false } as any);
      expect(results.some((r) => r.ruleId === 'NEXT218')).toBe(true);
    });

    it('flags `experimental.serverActions: true`', async () => {
      writeFile(
        'next.config.js',
        "module.exports = { experimental: { serverActions: true } };\n"
      );
      const scanner = new FrameworkScanner();
      const results = await scanner.scan({ directory: dir, verbose: false } as any);
      expect(results.some((r) => r.ruleId === 'NEXT219')).toBe(true);
    });

    it('does not flag a clean next.config', async () => {
      writeFile(
        'next.config.js',
        "module.exports = { reactStrictMode: true };\n"
      );
      const scanner = new FrameworkScanner();
      const results = await scanner.scan({ directory: dir, verbose: false } as any);
      expect(results.some((r) => r.ruleId === 'NEXT218' || r.ruleId === 'NEXT219')).toBe(false);
    });
  });
});
