/**
 * @fileoverview Tests for FileWatcher
 */

import { EventEmitter } from 'events';
import path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { FileChangeEvent, FileChangeType, FileWatcher } from './file-watcher.js';

// Mock fs module
vi.mock('fs', () => ({
  watch: vi.fn(() => ({
    close: vi.fn(),
    on: vi.fn()
  }))
}));

// Mock utils
vi.mock('../utils/fs.js', () => ({
  fileExists: vi.fn(() => Promise.resolve(true)),
  readFile: vi.fn(() => Promise.resolve(''))
}));

vi.mock('../utils/index.js', () => ({
  logger: {
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn(),
    success: vi.fn()
  }
}));

describe('FileWatcher', () => {
  let fileWatcher: FileWatcher;
  let mockWatch: any;

  beforeEach(() => {
    vi.clearAllMocks();
    mockWatch = vi.fn(() => ({
      close: vi.fn(),
      on: vi.fn()
    }));
    vi.doMock('fs', () => ({ watch: mockWatch }));
  });

  afterEach(async () => {
    if (fileWatcher) {
      await fileWatcher.stop();
    }
  });

  describe('constructor', () => {
    it('should create FileWatcher with default options', () => {
      fileWatcher = new FileWatcher({ watchDir: '/test' });
      expect(fileWatcher).toBeInstanceOf(FileWatcher);
      expect(fileWatcher).toBeInstanceOf(EventEmitter);
    });

    it('should merge provided options with defaults', () => {
      const options = {
        watchDir: '/custom',
        patterns: ['*.custom'],
        debounceMs: 200
      };

      fileWatcher = new FileWatcher(options);
      expect(fileWatcher).toBeInstanceOf(FileWatcher);
    });
  });

  describe('start', () => {
    beforeEach(() => {
      fileWatcher = new FileWatcher({ watchDir: '/test' });
    });

    it('should start watching successfully', async () => {
      await fileWatcher.start();
      expect(mockWatch).toHaveBeenCalled();
    });

    it('should not start if already watching', async () => {
      await fileWatcher.start();
      await fileWatcher.start(); // Second call should be ignored
      expect(mockWatch).toHaveBeenCalledTimes(1);
    });

    it('should throw error if watch directory does not exist', async () => {
      const { fileExists } = await import('../utils/fs.js');
      vi.mocked(fileExists).mockResolvedValueOnce(false);

      await expect(fileWatcher.start()).rejects.toThrow('Watch directory does not exist');
    });
  });

  describe('stop', () => {
    beforeEach(() => {
      fileWatcher = new FileWatcher({ watchDir: '/test' });
    });

    it('should stop watching successfully', async () => {
      const mockWatcher = {
        close: vi.fn(),
        on: vi.fn()
      };
      mockWatch.mockReturnValue(mockWatcher);

      await fileWatcher.start();
      await fileWatcher.stop();

      expect(mockWatcher.close).toHaveBeenCalled();
    });

    it('should not stop if not watching', async () => {
      await fileWatcher.stop(); // Should not throw
    });
  });

  describe('dependency tracking', () => {
    beforeEach(() => {
      fileWatcher = new FileWatcher({ watchDir: '/test' });
    });

    it('should add dependency relationships', () => {
      const dependent = '/test/component.ordo';
      const dependency = '/test/utils.ts';

      fileWatcher.addDependency(dependent, dependency);

      const depInfo = fileWatcher.getDependencyInfo(dependency);
      expect(depInfo?.dependents.has(path.resolve(dependent))).toBe(true);

      const dependentInfo = fileWatcher.getDependencyInfo(dependent);
      expect(dependentInfo?.dependencies.has(path.resolve(dependency))).toBe(true);
    });

    it('should remove dependency relationships', () => {
      const dependent = '/test/component.ordo';
      const dependency = '/test/utils.ts';

      fileWatcher.addDependency(dependent, dependency);
      fileWatcher.removeDependency(dependent, dependency);

      const depInfo = fileWatcher.getDependencyInfo(dependency);
      expect(depInfo?.dependents.has(path.resolve(dependent))).toBe(false);

      const dependentInfo = fileWatcher.getDependencyInfo(dependent);
      expect(dependentInfo?.dependencies.has(path.resolve(dependency))).toBe(false);
    });

    it('should get affected files including dependents', () => {
      const file1 = '/test/utils.ts';
      const file2 = '/test/component.ordo';
      const file3 = '/test/page.ordo';

      // Set up dependency chain: file1 <- file2 <- file3
      fileWatcher.addDependency(file2, file1);
      fileWatcher.addDependency(file3, file2);

      const affected = fileWatcher.getAffectedFiles(file1);

      expect(affected).toContain(path.resolve(file1));
      expect(affected).toContain(path.resolve(file2));
      expect(affected).toContain(path.resolve(file3));
    });
  });

  describe('file pattern matching', () => {
    beforeEach(() => {
      fileWatcher = new FileWatcher({
        watchDir: '/test',
        patterns: ['**/*.ordo', '**/*.ts'],
        ignorePatterns: ['**/node_modules/**', '**/dist/**']
      });
    });

    it('should match included patterns', () => {
      // Access private method for testing
      const shouldWatch = (fileWatcher as any).shouldWatchFile.bind(fileWatcher);

      expect(shouldWatch('/test/component.ordo')).toBe(true);
      expect(shouldWatch('/test/utils.ts')).toBe(true);
      expect(shouldWatch('/test/nested/component.ordo')).toBe(true);
    });

    it('should ignore excluded patterns', () => {
      const shouldWatch = (fileWatcher as any).shouldWatchFile.bind(fileWatcher);

      expect(shouldWatch('/test/node_modules/package.json')).toBe(false);
      expect(shouldWatch('/test/dist/bundle.js')).toBe(false);
    });

    it('should not match non-included patterns', () => {
      const shouldWatch = (fileWatcher as any).shouldWatchFile.bind(fileWatcher);

      expect(shouldWatch('/test/readme.md')).toBe(false);
      expect(shouldWatch('/test/image.png')).toBe(false);
    });
  });

  describe('change event handling', () => {
    beforeEach(() => {
      fileWatcher = new FileWatcher({ watchDir: '/test', debounceMs: 10 });
    });

    it('should emit change events for file modifications', async () => {
      const changePromise = new Promise<FileChangeEvent>((resolve) => {
        fileWatcher.once('change', resolve);
      });

      // Simulate file change
      const handleChange = (fileWatcher as any).handleFileChange.bind(fileWatcher);
      handleChange('change', '/test/component.ordo');

      // Wait for debounce
      await new Promise(resolve => setTimeout(resolve, 20));

      const changeEvent = await changePromise;
      expect(changeEvent.type).toBe(FileChangeType.CHANGED);
      expect(changeEvent.filePath).toBe(path.resolve('/test/component.ordo'));
      expect(changeEvent.extension).toBe('.ordo');
    });

    it('should debounce rapid file changes', async () => {
      const changes: FileChangeEvent[] = [];
      fileWatcher.on('change', (event) => changes.push(event));

      const handleChange = (fileWatcher as any).handleFileChange.bind(fileWatcher);

      // Simulate rapid changes
      handleChange('change', '/test/component.ordo');
      handleChange('change', '/test/component.ordo');
      handleChange('change', '/test/component.ordo');

      // Wait for debounce
      await new Promise(resolve => setTimeout(resolve, 20));

      // Should only emit one change event
      expect(changes).toHaveLength(1);
    });

    it('should handle file deletion events', async () => {
      const { fileExists } = await import('../utils/fs.js');
      vi.mocked(fileExists).mockResolvedValueOnce(false);

      const changePromise = new Promise<FileChangeEvent>((resolve) => {
        fileWatcher.once('change', resolve);
      });

      const handleChange = (fileWatcher as any).handleFileChange.bind(fileWatcher);
      handleChange('rename', '/test/component.ordo');

      await new Promise(resolve => setTimeout(resolve, 20));

      const changeEvent = await changePromise;
      expect(changeEvent.type).toBe(FileChangeType.DELETED);
    });

    it('should handle file addition events', async () => {
      const { fileExists } = await import('../utils/fs.js');
      vi.mocked(fileExists).mockResolvedValueOnce(true);

      const changePromise = new Promise<FileChangeEvent>((resolve) => {
        fileWatcher.once('change', resolve);
      });

      const handleChange = (fileWatcher as any).handleFileChange.bind(fileWatcher);
      handleChange('rename', '/test/new-component.ordo');

      await new Promise(resolve => setTimeout(resolve, 20));

      const changeEvent = await changePromise;
      expect(changeEvent.type).toBe(FileChangeType.ADDED);
    });
  });

  describe('error handling', () => {
    beforeEach(() => {
      fileWatcher = new FileWatcher({ watchDir: '/test' });
    });

    it('should emit error events from watcher', async () => {
      const mockWatcher = {
        close: vi.fn(),
        on: vi.fn()
      };
      mockWatch.mockReturnValue(mockWatcher);

      await fileWatcher.start();

      const errorPromise = new Promise<Error>((resolve) => {
        fileWatcher.once('error', resolve);
      });

      // Simulate watcher error
      const errorCallback = mockWatcher.on.mock.calls.find(call => call[0] === 'error')?.[1];
      const testError = new Error('Watcher error');
      errorCallback?.(testError);

      const error = await errorPromise;
      expect(error).toBe(testError);
    });

    it('should handle errors during file change processing', async () => {
      const { fileExists } = await import('../utils/fs.js');
      vi.mocked(fileExists).mockRejectedValueOnce(new Error('File system error'));

      const errorPromise = new Promise<Error>((resolve) => {
        fileWatcher.once('error', resolve);
      });

      const handleChange = (fileWatcher as any).handleFileChange.bind(fileWatcher);
      handleChange('change', '/test/component.ordo');

      await new Promise(resolve => setTimeout(resolve, 20));

      const error = await errorPromise;
      expect(error).toBeInstanceOf(Error);
    });
  });
});
