import { describe, it, expect } from 'vitest';
import {
  isSecretPath,
  filterSecretEntries,
  SecretBlockedError,
  DEFAULT_SECURITY_IGNORE_FILETYPES,
} from '../secret_guard.js';

// ---- env / config patterns -------------------------------------------------

describe('isSecretPath — env / config files', () => {
  it('matches .env (literal via .env*)', () => {
    expect(isSecretPath('.env')).toBe(true);
  });

  it('matches .env.local (via .env*)', () => {
    expect(isSecretPath('.env.local')).toBe(true);
  });

  it('matches .env.production.local (via .env*)', () => {
    expect(isSecretPath('.env.production.local')).toBe(true);
  });

  it('matches .envrc (via .env*, common direnv config holding secrets)', () => {
    expect(isSecretPath('.envrc')).toBe(true);
  });

  it('matches nested .env (apps/server/.env via basename test)', () => {
    expect(isSecretPath('apps/server/.env')).toBe(true);
  });

  it('case-insensitive: .ENV matches .env*', () => {
    expect(isSecretPath('.ENV')).toBe(true);
  });
});

// ---- SSH / cert / key patterns --------------------------------------------

describe('isSecretPath — SSH / certs / keys', () => {
  it('matches id_rsa (continue.dev literal)', () => {
    expect(isSecretPath('id_rsa')).toBe(true);
  });

  it('matches id_rsa.pub (BooCode addition id_rsa*)', () => {
    // continue.dev's literal id_rsa wouldn't match this; BooCode broadens
    // because .pub files leak hostnames/usernames and authorized_keys hints.
    expect(isSecretPath('id_rsa.pub')).toBe(true);
  });

  it('matches cert.pem (*.pem)', () => {
    expect(isSecretPath('cert.pem')).toBe(true);
  });

  it('matches private.key (*.key)', () => {
    expect(isSecretPath('private.key')).toBe(true);
  });
});

// ---- credential patterns ---------------------------------------------------

describe('isSecretPath — credential files (BooCode additions)', () => {
  it('matches credentials.json (BooCode *credentials*)', () => {
    expect(isSecretPath('credentials.json')).toBe(true);
  });

  it('matches aws_credentials (BooCode *credentials* — substring match)', () => {
    // continue.dev has no `credentials*` pattern. BooCode adds `*credentials*`
    // to catch the common `aws_credentials`, `gcp-credentials.yml`, etc.
    expect(isSecretPath('aws_credentials')).toBe(true);
  });

  it('matches .netrc (BooCode addition)', () => {
    expect(isSecretPath('.netrc')).toBe(true);
  });

  it('matches keystore.kdbx (BooCode addition *.kdbx)', () => {
    expect(isSecretPath('keystore.kdbx')).toBe(true);
  });
});

// ---- directory patterns ----------------------------------------------------

describe('isSecretPath — directory segments (trailing-slash patterns)', () => {
  it('matches files under .aws/ via segment test', () => {
    expect(isSecretPath('home/user/.aws/credentials')).toBe(true);
  });

  it('matches files under .ssh/', () => {
    expect(isSecretPath('home/user/.ssh/known_hosts')).toBe(true);
  });

  it('matches files inside any path segment named secrets/', () => {
    expect(isSecretPath('apps/server/secrets/api.key')).toBe(true);
  });
});

// ---- negatives -------------------------------------------------------------

describe('isSecretPath — negatives', () => {
  it('package.json is allowed', () => {
    expect(isSecretPath('package.json')).toBe(false);
  });

  it('README.md is allowed', () => {
    expect(isSecretPath('README.md')).toBe(false);
  });

  it('Login.tsx is allowed (substring "login" doesn\'t trigger anything)', () => {
    expect(isSecretPath('src/components/Login.tsx')).toBe(false);
  });

  it('empty string returns false (defensive)', () => {
    expect(isSecretPath('')).toBe(false);
  });

  it('a directory NAMED "credentials" alone does NOT trigger — only file basenames do', () => {
    // Worth pinning: BooCode's `*credentials*` is a basename pattern (no
    // trailing `/`), so it tests the leaf filename only. A directory
    // literally called "credentials" containing innocuous files (e.g.
    // Login.tsx) is fine. This is a deliberate trade-off vs. continue.dev's
    // dir-pattern approach — adding `credentials/` as a dir pattern would
    // block legitimate code like `src/auth/credentials/Login.tsx`.
    expect(isSecretPath('src/auth/credentials/Login.tsx')).toBe(false);
    // ...but a file INSIDE that dir whose name includes "credentials" still
    // blocks via the basename match:
    expect(isSecretPath('src/auth/credentials/credentials.ts')).toBe(true);
  });
});

// ---- filterSecretEntries (listing-tools helper) ----------------------------

describe('filterSecretEntries', () => {
  it('removes secret entries and reports the count via note string', () => {
    const entries = [
      { path: 'src/index.ts' },
      { path: '.env' },
      { path: 'README.md' },
      { path: 'id_rsa' },
      { path: 'apps/server/package.json' },
    ];
    const result = filterSecretEntries(entries, (e) => e.path);
    expect(result.kept.map((e) => e.path)).toEqual([
      'src/index.ts',
      'README.md',
      'apps/server/package.json',
    ]);
    expect(result.hidden).toBe(2);
    expect(result.note).toBe('[pathGuard: 2 entries hidden by secret-file filter]');
  });

  it('returns undefined note when nothing was filtered', () => {
    const result = filterSecretEntries(
      [{ path: 'a.ts' }, { path: 'b.ts' }],
      (e) => e.path,
    );
    expect(result.kept).toHaveLength(2);
    expect(result.hidden).toBe(0);
    expect(result.note).toBeUndefined();
  });

  it('uses singular "entry" for a 1-hit filter (cosmetic but worth pinning)', () => {
    const result = filterSecretEntries(
      [{ path: 'index.ts' }, { path: '.env' }],
      (e) => e.path,
    );
    expect(result.note).toBe('[pathGuard: 1 entry hidden by secret-file filter]');
  });
});

// ---- SecretBlockedError ----------------------------------------------------

describe('SecretBlockedError', () => {
  it('carries the offending path on .path and in the message', () => {
    const err = new SecretBlockedError('apps/server/.env');
    expect(err.name).toBe('SecretBlockedError');
    expect(err.path).toBe('apps/server/.env');
    expect(err.message).toContain('apps/server/.env');
    expect(err.message).toContain('pathGuard');
  });
});

// ---- contract sanity check -------------------------------------------------

describe('DEFAULT_SECURITY_IGNORE_FILETYPES', () => {
  it('exports at least 40 patterns (continue.dev base) and is non-empty', () => {
    expect(DEFAULT_SECURITY_IGNORE_FILETYPES.length).toBeGreaterThanOrEqual(40);
  });

  it('includes all the headline continue.dev entries we tested above', () => {
    // Spot-check that the list still carries the patterns whose behavior
    // the tests depend on. Catches an accidental list edit that would
    // silently degrade coverage.
    const set = new Set(DEFAULT_SECURITY_IGNORE_FILETYPES);
    for (const pat of ['*.env', '.env*', '*.pem', '*.key', 'id_rsa', '.aws/', '.ssh/']) {
      expect(set.has(pat), `missing pattern: ${pat}`).toBe(true);
    }
  });
});
