/**
 * Security tests for permission bypass prevention
 *
 * Validates protection against:
 * - Nested sensitive file patterns (.env.backup, .env.local.bak)
 * - Multi-level symlink chains
 * - TOCTOU race conditions (time-of-check to time-of-use)
 * - Hardlink redirect attacks
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
  validatePathSecure,
  isSensitivePath,
} from '../../../src/core/write/writer.js';
import {
  createTempVault,
  cleanupTempVault,
  createTestNote,
} from '../helpers/testUtils.js';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';

describe('Permission Bypass Prevention', () => {
  let tempVault: string;

  beforeEach(async () => {
    tempVault = await createTempVault();
  });

  afterEach(async () => {
    await cleanupTempVault(tempVault);
  });

  // ========================================
  // Nested Sensitive File Patterns
  // ========================================

  describe('Nested sensitive file patterns', () => {
    it('should block .env.backup files', () => {
      expect(isSensitivePath('.env.backup')).toBe(true);
      expect(isSensitivePath('config/.env.backup')).toBe(true);
    });

    it('should block .env.local.bak files', () => {
      expect(isSensitivePath('.env.local.bak')).toBe(true);
      expect(isSensitivePath('backup/.env.local.bak')).toBe(true);
    });

    it('should block .env.old files', () => {
      expect(isSensitivePath('.env.old')).toBe(true);
      expect(isSensitivePath('.env.old.1')).toBe(true);
    });

    it('should block .env with various suffixes', () => {
      // Common backup/copy suffixes
      expect(isSensitivePath('.env~')).toBe(true);
      expect(isSensitivePath('.env.swp')).toBe(true);
      expect(isSensitivePath('.env.swo')).toBe(true);
      expect(isSensitivePath('.env.copy')).toBe(true);
      expect(isSensitivePath('.env.orig')).toBe(true);
    });

    it('should block deeply nested .env files', () => {
      expect(isSensitivePath('a/b/c/d/.env')).toBe(true);
      expect(isSensitivePath('config/env/.env.local')).toBe(true);
    });

    it('should block credentials with various extensions', () => {
      expect(isSensitivePath('credentials.json.bak')).toBe(true);
      expect(isSensitivePath('credentials.json.backup')).toBe(true);
      expect(isSensitivePath('credentials.json~')).toBe(true);
    });

    it('should block secrets files with backup extensions', () => {
      expect(isSensitivePath('secrets.yaml.bak')).toBe(true);
      expect(isSensitivePath('secrets.yml.backup')).toBe(true);
      expect(isSensitivePath('secrets.yaml.old')).toBe(true);
    });

    it('should block key files with backup extensions', () => {
      expect(isSensitivePath('private.key.bak')).toBe(true);
      expect(isSensitivePath('server.pem.backup')).toBe(true);
      expect(isSensitivePath('cert.p12.old')).toBe(true);
    });

    it('should block SSH key copies', () => {
      expect(isSensitivePath('id_rsa.bak')).toBe(true);
      expect(isSensitivePath('id_ed25519.backup')).toBe(true);
      expect(isSensitivePath('.ssh/id_rsa.old')).toBe(true);
    });
  });

  // ========================================
  // Symlink Attack Prevention
  // ========================================

  describe('Symlink attack prevention', () => {
    it('should detect symlink pointing outside vault', async () => {
      // Create a symlink pointing outside the vault
      const outsideDir = await fs.mkdtemp(path.join(os.tmpdir(), 'outside-'));
      const outsideFile = path.join(outsideDir, 'secret.txt');
      await fs.writeFile(outsideFile, 'secret content');

      try {
        // Create symlink in vault pointing to outside file
        const symlinkPath = path.join(tempVault, 'escape-link');
        await fs.symlink(outsideFile, symlinkPath);

        const result = await validatePathSecure(tempVault, 'escape-link');
        expect(result.valid).toBe(false);
        expect(result.reason).toContain('outside vault');
      } finally {
        await fs.rm(outsideDir, { recursive: true, force: true });
      }
    });

    it('should detect symlinked directory pointing outside vault', async () => {
      const outsideDir = await fs.mkdtemp(path.join(os.tmpdir(), 'outside-'));
      await fs.writeFile(path.join(outsideDir, 'data.md'), '# Data');

      try {
        // Create symlink to outside directory
        const symlinkPath = path.join(tempVault, 'escape-dir');
        await fs.symlink(outsideDir, symlinkPath);

        const result = await validatePathSecure(tempVault, 'escape-dir/data.md');
        expect(result.valid).toBe(false);
        expect(result.reason).toContain('outside vault');
      } finally {
        await fs.rm(outsideDir, { recursive: true, force: true });
      }
    });

    it('should detect multi-level symlink chains escaping vault', async () => {
      const outsideDir = await fs.mkdtemp(path.join(os.tmpdir(), 'outside-'));
      const secretFile = path.join(outsideDir, 'secret.txt');
      await fs.writeFile(secretFile, 'secret');

      try {
        // Create chain: vault/link1 -> vault/link2 -> outside/secret
        await fs.mkdir(path.join(tempVault, 'subdir'), { recursive: true });

        // link2 points outside
        const link2Path = path.join(tempVault, 'subdir', 'link2');
        await fs.symlink(secretFile, link2Path);

        // link1 points to link2
        const link1Path = path.join(tempVault, 'link1');
        await fs.symlink(link2Path, link1Path);

        // Both should be blocked
        const result1 = await validatePathSecure(tempVault, 'link1');
        expect(result1.valid).toBe(false);

        const result2 = await validatePathSecure(tempVault, 'subdir/link2');
        expect(result2.valid).toBe(false);
      } finally {
        await fs.rm(outsideDir, { recursive: true, force: true });
      }
    });

    it('should allow symlinks within the vault', async () => {
      // Create a legitimate file
      await createTestNote(tempVault, 'notes/real-note.md', '# Real Note\n\nContent');

      // Create symlink within vault
      const symlinkPath = path.join(tempVault, 'linked-note.md');
      await fs.symlink(path.join(tempVault, 'notes', 'real-note.md'), symlinkPath);

      const result = await validatePathSecure(tempVault, 'linked-note.md');
      expect(result.valid).toBe(true);
    });

    it('should detect symlink to sensitive file within vault', async () => {
      // Create a sensitive file in vault (shouldn't happen but test anyway)
      await fs.writeFile(path.join(tempVault, '.env'), 'SECRET=value');

      // Create symlink to it
      const symlinkPath = path.join(tempVault, 'notes', 'innocent-link.md');
      await fs.mkdir(path.join(tempVault, 'notes'), { recursive: true });
      await fs.symlink(path.join(tempVault, '.env'), symlinkPath);

      const result = await validatePathSecure(tempVault, 'notes/innocent-link.md');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('sensitive');
    });

    it('should handle circular symlinks gracefully', async () => {
      // Create circular symlink: A -> B -> A
      const linkAPath = path.join(tempVault, 'linkA');
      const linkBPath = path.join(tempVault, 'linkB');

      try {
        // Note: This may fail on some systems, that's OK
        await fs.symlink(linkBPath, linkAPath);
        await fs.symlink(linkAPath, linkBPath);

        // Should not hang, should return invalid or error
        const result = await validatePathSecure(tempVault, 'linkA');
        // Either invalid or error is acceptable for circular symlinks
        expect(typeof result.valid).toBe('boolean');
      } catch {
        // Some systems prevent creating circular symlinks, which is fine
        expect(true).toBe(true);
      }
    });
  });

  // ========================================
  // Parent Directory Symlink Attacks
  // ========================================

  describe('Parent directory symlink attacks', () => {
    it('should detect symlinked parent directory pointing outside', async () => {
      const outsideDir = await fs.mkdtemp(path.join(os.tmpdir(), 'outside-'));
      await fs.writeFile(path.join(outsideDir, 'data.md'), '# External Data');

      try {
        // Create parent directory as symlink
        const escapeDirPath = path.join(tempVault, 'escape-parent');
        await fs.symlink(outsideDir, escapeDirPath);

        // Try to access file through symlinked parent
        const result = await validatePathSecure(tempVault, 'escape-parent/data.md');
        expect(result.valid).toBe(false);
        expect(result.reason).toContain('outside vault');
      } finally {
        await fs.rm(outsideDir, { recursive: true, force: true });
      }
    });

    it('should detect deeply nested symlinked parent', async () => {
      const outsideDir = await fs.mkdtemp(path.join(os.tmpdir(), 'outside-'));
      await fs.writeFile(path.join(outsideDir, 'secret.md'), '# Secret');

      try {
        // Create a/b/c where b is a symlink outside
        await fs.mkdir(path.join(tempVault, 'a'), { recursive: true });
        const linkPath = path.join(tempVault, 'a', 'b');
        await fs.symlink(outsideDir, linkPath);

        const result = await validatePathSecure(tempVault, 'a/b/secret.md');
        expect(result.valid).toBe(false);
      } finally {
        await fs.rm(outsideDir, { recursive: true, force: true });
      }
    });
  });

  // ========================================
  // Hidden File Detection
  // ========================================

  describe('Hidden file patterns', () => {
    it('should allow normal hidden files', async () => {
      const result = await validatePathSecure(tempVault, '.hidden-note.md');
      expect(result.valid).toBe(true);
    });

    it('should block hidden sensitive files', () => {
      expect(isSensitivePath('.env.secret')).toBe(true);
      expect(isSensitivePath('.credentials')).toBe(true);
    });

    it('should handle double-dot hidden files', async () => {
      // ..filename is NOT traversal — it resolves inside the vault as a
      // legitimate filename. Only '..' and '../' are traversal.
      const result = await validatePathSecure(tempVault, '..weird-name.md');
      expect(result.valid).toBe(true);
    });

    it('should handle hidden directories with sensitive files', () => {
      expect(isSensitivePath('.config/.env')).toBe(true);
      expect(isSensitivePath('.secret/credentials.json')).toBe(true);
    });
  });

  // ========================================
  // Case Sensitivity Bypass
  // ========================================

  describe('Case sensitivity bypass attempts', () => {
    it('should block .ENV regardless of case', () => {
      expect(isSensitivePath('.ENV')).toBe(true);
      expect(isSensitivePath('.Env')).toBe(true);
      expect(isSensitivePath('.eNv')).toBe(true);
    });

    it('should block CREDENTIALS.JSON regardless of case', () => {
      expect(isSensitivePath('CREDENTIALS.JSON')).toBe(true);
      expect(isSensitivePath('Credentials.Json')).toBe(true);
      expect(isSensitivePath('cReDeNtIaLs.JsOn')).toBe(true);
    });

    it('should block ID_RSA regardless of case', () => {
      expect(isSensitivePath('ID_RSA')).toBe(true);
      expect(isSensitivePath('Id_Rsa')).toBe(true);
      expect(isSensitivePath('iD_rSa')).toBe(true);
    });

    it('should block .PEM files regardless of case', () => {
      expect(isSensitivePath('cert.PEM')).toBe(true);
      expect(isSensitivePath('cert.Pem')).toBe(true);
      expect(isSensitivePath('CERT.pem')).toBe(true);
    });
  });

  // ========================================
  // Null Byte and Special Character Bypass
  // ========================================

  describe('Null byte and special character bypass', () => {
    it('should handle null byte in path', async () => {
      const result = await validatePathSecure(tempVault, 'file\x00.env');
      // Null bytes in paths are suspicious, should be handled safely
      expect(typeof result.valid).toBe('boolean');
    });

    it('should handle paths with control characters', async () => {
      // Bell character, tab, carriage return
      const weirdPaths = [
        'file\x07.md',
        'file\t.md',
        'file\r.md',
      ];

      for (const weirdPath of weirdPaths) {
        const result = await validatePathSecure(tempVault, weirdPath);
        expect(typeof result.valid).toBe('boolean');
      }
    });
  });

  // ========================================
  // Extension Confusion
  // ========================================

  describe('Extension confusion attacks', () => {
    it('should detect double extensions hiding sensitive files', () => {
      // file.md.env should be blocked
      expect(isSensitivePath('note.md.env')).toBe(true);
      expect(isSensitivePath('readme.md.key')).toBe(true);
    });

    it('should handle extremely long extensions', () => {
      const longExt = 'file' + '.md'.repeat(100);
      const result = isSensitivePath(longExt);
      expect(typeof result).toBe('boolean');
    });

    it('should handle files with no extension', () => {
      // passwd, shadow have no extension
      expect(isSensitivePath('passwd')).toBe(true);
      expect(isSensitivePath('shadow')).toBe(true);
    });

    it('should handle multiple dots in filename', () => {
      expect(isSensitivePath('file.backup.env.local')).toBe(true);
      expect(isSensitivePath('2026.01.28.credentials.json')).toBe(true);
    });
  });

  // ========================================
  // Directory Traversal via Sensitive Check
  // ========================================

  describe('Traversal combined with sensitive check', () => {
    it('should block traversal to system sensitive files', () => {
      expect(isSensitivePath('/etc/passwd')).toBe(true);
      expect(isSensitivePath('/etc/shadow')).toBe(true);
    });

    it('should handle Windows system paths', () => {
      expect(isSensitivePath('C:\\Windows\\System32\\config\\SAM')).toBe(false); // Not in pattern list
      // But the path validation should block this separately
    });

    it('should block relative paths to git config', () => {
      expect(isSensitivePath('../.git/config')).toBe(true);
      expect(isSensitivePath('../../.git/credentials')).toBe(true);
    });
  });

  // ========================================
  // Race Condition Simulation
  // ========================================

  describe('TOCTOU awareness', () => {
    it('should validate at write time, not just check time', async () => {
      // This test documents that validatePathSecure is async
      // and resolves symlinks at validation time

      const filePath = path.join(tempVault, 'note.md');
      await fs.writeFile(filePath, '# Note');

      // First validation passes
      const result1 = await validatePathSecure(tempVault, 'note.md');
      expect(result1.valid).toBe(true);

      // In a real TOCTOU scenario, the file could change between
      // validation and write. The writeVaultFile function calls
      // validatePathSecure internally, providing protection.

      // This test verifies the validation is async and complete
      expect(typeof result1.valid).toBe('boolean');
    });

    it('should handle non-existent files correctly', async () => {
      // Validating a path that doesn't exist yet
      const result = await validatePathSecure(tempVault, 'new-note.md');
      expect(result.valid).toBe(true);
    });

    it('should handle non-existent parent directories', async () => {
      const result = await validatePathSecure(tempVault, 'new-dir/sub-dir/note.md');
      expect(result.valid).toBe(true);
    });
  });

  // ========================================
  // Permission Escalation Patterns
  // ========================================

  describe('Permission escalation patterns', () => {
    it('should block .npmrc files', () => {
      // Contains npm tokens
      expect(isSensitivePath('.npmrc')).toBe(true);
    });

    it('should block .netrc files', () => {
      // Contains credentials for various services
      expect(isSensitivePath('.netrc')).toBe(true);
    });

    it('should block AWS credential files', () => {
      expect(isSensitivePath('.aws/credentials')).toBe(true);
      expect(isSensitivePath('.aws/config')).toBe(true);
    });

    it('should block GCP credential files', () => {
      expect(isSensitivePath('gcloud/credentials.json')).toBe(true);
      expect(isSensitivePath('.config/gcloud/credentials.json')).toBe(true);
    });

    it('should block Azure credential files', () => {
      expect(isSensitivePath('.azure/credentials')).toBe(true);
    });

    it('should block Docker config', () => {
      expect(isSensitivePath('.docker/config.json')).toBe(true);
    });

    it('should block Kubernetes config', () => {
      expect(isSensitivePath('.kube/config')).toBe(true);
    });
  });
});
