import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import { mkdirSafe, existsSafe } from '../src/fs-utils';

describe('fs-utils.ts', () => {
  let tempDir: string;

  beforeEach(async () => {
    // Create a temporary directory for testing
    tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fs-utils-test-'));
  });

  afterEach(async () => {
    // Clean up temporary directory
    try {
      await fs.rm(tempDir, { recursive: true, force: true });
    } catch (error) {
      // Ignore cleanup errors
    }
  });

  describe('mkdirSafe', () => {
    it('should create a new directory', async () => {
      const newDir = path.join(tempDir, 'new-directory');

      await mkdirSafe(newDir);

      const stats = await fs.stat(newDir);
      expect(stats.isDirectory()).toBe(true);
    });

    it('should create nested directories recursively', async () => {
      const nestedDir = path.join(tempDir, 'level1', 'level2', 'level3');

      await mkdirSafe(nestedDir);

      const stats = await fs.stat(nestedDir);
      expect(stats.isDirectory()).toBe(true);
    });

    it('should succeed when directory already exists', async () => {
      const existingDir = path.join(tempDir, 'existing');
      await fs.mkdir(existingDir);

      await expect(mkdirSafe(existingDir)).resolves.not.toThrow();
    });

    it('should succeed when path is a symlink to a directory', async () => {
      const targetDir = path.join(tempDir, 'target');
      const symlinkPath = path.join(tempDir, 'symlink');

      await fs.mkdir(targetDir);
      await fs.symlink(targetDir, symlinkPath, 'dir');

      await expect(mkdirSafe(symlinkPath)).resolves.not.toThrow();
    });

    it('should throw when path exists as a file', async () => {
      const filePath = path.join(tempDir, 'existing-file.txt');
      await fs.writeFile(filePath, 'content');

      await expect(mkdirSafe(filePath)).rejects.toThrow('Path exists and is not a directory');
    });

    it('should resolve relative paths', async () => {
      const originalCwd = process.cwd();
      try {
        process.chdir(tempDir);

        await mkdirSafe('relative-dir');

        const stats = await fs.stat(path.join(tempDir, 'relative-dir'));
        expect(stats.isDirectory()).toBe(true);
      } finally {
        process.chdir(originalCwd);
      }
    });

    it('should handle concurrent creation gracefully', async () => {
      const newDir = path.join(tempDir, 'concurrent');

      const promises = [mkdirSafe(newDir), mkdirSafe(newDir), mkdirSafe(newDir)];

      await expect(Promise.all(promises)).resolves.not.toThrow();

      const stats = await fs.stat(newDir);
      expect(stats.isDirectory()).toBe(true);
    });

    it('should handle paths with special characters', async () => {
      const specialDir = path.join(tempDir, 'dir with spaces & special-chars');

      await mkdirSafe(specialDir);

      const stats = await fs.stat(specialDir);
      expect(stats.isDirectory()).toBe(true);
    });
  });

  describe('existsSafe', () => {
    it('should return true for existing directory', async () => {
      const existingDir = path.join(tempDir, 'exists');
      await fs.mkdir(existingDir);

      const result = await existsSafe(existingDir);

      expect(result).toBe(true);
    });

    it('should return true for existing file', async () => {
      const filePath = path.join(tempDir, 'file.txt');
      await fs.writeFile(filePath, 'content');

      const result = await existsSafe(filePath);

      expect(result).toBe(true);
    });

    it('should return false for non-existent path', async () => {
      const nonExistent = path.join(tempDir, 'does-not-exist');

      const result = await existsSafe(nonExistent);

      expect(result).toBe(false);
    });

    it('should return true for symlink', async () => {
      const target = path.join(tempDir, 'target.txt');
      const symlink = path.join(tempDir, 'link.txt');

      await fs.writeFile(target, 'content');
      await fs.symlink(target, symlink);

      const result = await existsSafe(symlink);

      expect(result).toBe(true);
    });

    it('should return true for broken symlink', async () => {
      const symlink = path.join(tempDir, 'broken-link');

      // Create symlink to non-existent target
      await fs.symlink('/non/existent/path', symlink);

      const result = await existsSafe(symlink);

      // lstat succeeds on broken symlinks
      expect(result).toBe(true);
    });

    it('should handle relative paths', async () => {
      const originalCwd = process.cwd();
      try {
        process.chdir(tempDir);

        await fs.writeFile('test.txt', 'content');

        const result = await existsSafe('test.txt');

        expect(result).toBe(true);
      } finally {
        process.chdir(originalCwd);
      }
    });

    it('should handle paths with special characters', async () => {
      const specialFile = path.join(tempDir, 'file with spaces & chars.txt');
      await fs.writeFile(specialFile, 'content');

      const result = await existsSafe(specialFile);

      expect(result).toBe(true);
    });

    it('should return false for parent of non-existent nested path', async () => {
      const nonExistent = path.join(tempDir, 'does', 'not', 'exist');

      const result = await existsSafe(nonExistent);

      expect(result).toBe(false);
    });
  });

  describe('Error handling', () => {
    it('should handle permission errors in mkdirSafe', async () => {
      // Skip on Windows where permissions work differently
      if (process.platform === 'win32') {
        return;
      }

      const restrictedDir = path.join(tempDir, 'restricted');
      await fs.mkdir(restrictedDir);

      // Make directory read-only
      await fs.chmod(restrictedDir, 0o444);

      const newDir = path.join(restrictedDir, 'new');

      try {
        await expect(mkdirSafe(newDir)).rejects.toThrow();
      } finally {
        // Restore permissions for cleanup
        await fs.chmod(restrictedDir, 0o755);
      }
    });

    it('should propagate unexpected errors in existsSafe', async () => {
      const mockError = new Error('Unexpected error');
      (mockError as any).code = 'EACCES';

      const spy = vi.spyOn(fs, 'lstat').mockRejectedValueOnce(mockError);

      await expect(existsSafe('/some/path')).rejects.toThrow('Unexpected error');

      spy.mockRestore();
    });
  });

  describe('Integration scenarios', () => {
    it('should work together: check existence then create if missing', async () => {
      const dirPath = path.join(tempDir, 'check-then-create');

      const existsBefore = await existsSafe(dirPath);
      expect(existsBefore).toBe(false);

      await mkdirSafe(dirPath);

      const existsAfter = await existsSafe(dirPath);
      expect(existsAfter).toBe(true);
    });

    it('should handle nested directory creation with existence checks', async () => {
      const level1 = path.join(tempDir, 'level1');
      const level2 = path.join(level1, 'level2');
      const level3 = path.join(level2, 'level3');

      await mkdirSafe(level3);

      expect(await existsSafe(level1)).toBe(true);
      expect(await existsSafe(level2)).toBe(true);
      expect(await existsSafe(level3)).toBe(true);
    });
  });
});
