import { Test, TestingModule } from '@nestjs/testing';
import chokidar from 'chokidar';
import { DatasetsService } from './datasets.service';
import { DbService } from './db.service';
import { EventsGateway } from './events.gateway';
import { TaskQueueService } from './task-queue.service';
import { WatcherService } from './watcher.service';

// Mock chokidar
jest.mock('chokidar', () => ({
  watch: jest.fn(),
}));

// Mock fs
jest.mock('fs', () => ({
  statSync: jest.fn((path: string) => ({
    isDirectory: () => path.endsWith('subdir') || !path.includes('.'),
  })),
}));

// Mock Worker
jest.mock('worker_threads', () => ({
  Worker: jest.fn().mockImplementation(() => ({
    on: jest.fn(),
    postMessage: jest.fn(),
  })),
}));

// Mock DatasetsService
jest.mock('./datasets.service', () => ({
  DatasetsService: jest.fn().mockImplementation(() => ({
    getEnabledDatasetPaths: jest.fn(),
    getDatasetConfig: jest.fn(),
    getDatasetExts: jest.fn(),
  })),
}));

// Mock DbService
jest.mock('./db.service', () => ({
  DbService: jest.fn().mockImplementation(() => ({
    getDb: jest.fn(() => ({
      prepare: jest.fn(() => ({
        get: jest.fn(() => null),
        run: jest.fn(),
      })),
    })),
  })),
}));

// Mock EventsGateway
jest.mock('./events.gateway', () => ({
  EventsGateway: jest.fn().mockImplementation(() => ({
    emitFileUpdate: jest.fn(),
    emitWatcherUpdate: jest.fn(),
  })),
}));

// Mock TaskQueueService
jest.mock('./task-queue.service', () => ({
  TaskQueueService: jest.fn().mockImplementation(() => ({})),
}));

describe('WatcherService', () => {
  let service: WatcherService;
  let datasetsService: jest.Mocked<DatasetsService>;
  let dbService: jest.Mocked<DbService>;
  let eventsGateway: jest.Mocked<EventsGateway>;
  let taskQueueService: jest.Mocked<TaskQueueService>;

  beforeEach(async () => {
    const mockDatasetsService = {
      getEnabledDatasetPaths: jest.fn(),
      getDatasetConfig: jest.fn(),
      getDatasetExts: jest.fn(),
    };

    const mockDbService = {
      getDb: jest.fn(() => ({
        prepare: jest.fn(() => ({
          get: jest.fn(() => null),
          run: jest.fn(),
        })),
      })),
    };
    const mockEventsGateway = {
      emitFileUpdate: jest.fn(),
      emitWatcherUpdate: jest.fn(),
    };
    const mockTaskQueueService = {};

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        WatcherService,
        {
          provide: DatasetsService,
          useValue: mockDatasetsService,
        },
        {
          provide: DbService,
          useValue: mockDbService,
        },
        {
          provide: EventsGateway,
          useValue: mockEventsGateway,
        },
        {
          provide: TaskQueueService,
          useValue: mockTaskQueueService,
        },
      ],
    }).compile();

    service = module.get<WatcherService>(WatcherService);
    datasetsService = module.get(DatasetsService);
    dbService = module.get(DbService);
    eventsGateway = module.get(EventsGateway);
    taskQueueService = module.get(TaskQueueService);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('getDatasetExts', () => {
    it('should return exts array for a dataset with exts configured', () => {
      const mockConfig = {
        movies: {
          enabled: true,
          exts: ['.mkv', '.mp4', '.avi'],
          '/path/to/movies': {},
        },
      };

      datasetsService.getDatasetConfig.mockReturnValue(mockConfig);
      datasetsService.getDatasetExts.mockReturnValue(['.mkv', '.mp4', '.avi']);

      const exts = datasetsService.getDatasetExts('movies');
      expect(exts).toEqual(['.mkv', '.mp4', '.avi']);
    });

    it('should return null for a dataset without exts configured', () => {
      const mockConfig = {
        movies: {
          enabled: true,
          '/path/to/movies': {},
        },
      };

      datasetsService.getDatasetConfig.mockReturnValue(mockConfig);
      datasetsService.getDatasetExts.mockReturnValue(null);

      const exts = datasetsService.getDatasetExts('movies');
      expect(exts).toBeNull();
    });
  });

  describe('Chokidar options setup', () => {
    it('should set up ignored function to filter files by extensions', () => {
      const mockConfig = {
        movies: {
          enabled: true,
          '/path/to/movies': {
            exts: ['mkv', 'mp4'],
          },
        },
      };

      datasetsService.getEnabledDatasetPaths.mockReturnValue([
        '/path/to/movies',
      ]);
      datasetsService.getDatasetConfig.mockReturnValue(mockConfig);

      // Mock the getDatasetFromPath method to return 'movies' for our test path
      jest
        .spyOn(service as any, 'getDatasetFromPath')
        .mockReturnValue('movies');

      const mockWatcher = {
        on: jest.fn().mockReturnThis(),
      };
      (chokidar.watch as jest.Mock).mockReturnValue(mockWatcher);

      service.start();

      // Verify chokidar.watch was called
      expect(chokidar.watch).toHaveBeenCalledWith(
        ['/path/to/movies'],
        expect.objectContaining({
          ignored: expect.any(Function),
        }),
      );

      // Get the ignored function from the call
      const callArgs = (chokidar.watch as jest.Mock).mock.calls[0];
      const options = callArgs[1];
      const ignoredFunction = options.ignored;

      // Test the ignored function
      expect(ignoredFunction('/path/to/movies/movie.mkv')).toBe(false); // Should watch
      expect(ignoredFunction('/path/to/movies/movie.mp4')).toBe(false); // Should watch
      expect(ignoredFunction('/path/to/movies/movie.nfo')).toBe(true); // Should ignore
      expect(ignoredFunction('/path/to/movies/movie.txt')).toBe(true); // Should ignore
      expect(ignoredFunction('/path/to/movies/subdir')).toBe(false); // Should not ignore directories
    });

    it('should watch all files when no exts are configured', () => {
      const mockConfig = {
        movies: {
          enabled: true,
          '/path/to/movies': {},
        },
      };

      datasetsService.getEnabledDatasetPaths.mockReturnValue([
        '/path/to/movies',
      ]);
      datasetsService.getDatasetConfig.mockReturnValue(mockConfig);

      // Mock the getDatasetFromPath method to return 'movies' for our test path
      jest
        .spyOn(service as any, 'getDatasetFromPath')
        .mockReturnValue('movies');

      const mockWatcher = {
        on: jest.fn().mockReturnThis(),
      };
      (chokidar.watch as jest.Mock).mockReturnValue(mockWatcher);

      service.start();

      // Get the ignored function from the call
      const callArgs = (chokidar.watch as jest.Mock).mock.calls[0];
      const options = callArgs[1];
      const ignoredFunction = options.ignored;

      // Test the ignored function - should watch all files when no exts configured
      expect(ignoredFunction('/path/to/movies/movie.mkv')).toBe(false);
      expect(ignoredFunction('/path/to/movies/movie.nfo')).toBe(false);
      expect(ignoredFunction('/path/to/movies/movie.txt')).toBe(false);
    });

    it('should ignore files that do not belong to any dataset', () => {
      const mockConfig = {
        movies: {
          enabled: true,
          exts: ['.mkv', '.mp4'],
          '/path/to/movies': {},
        },
      };

      datasetsService.getEnabledDatasetPaths.mockReturnValue([
        '/path/to/movies',
      ]);
      datasetsService.getDatasetConfig.mockReturnValue(mockConfig);

      // Mock the getDatasetFromPath method to return null for files outside datasets
      jest.spyOn(service as any, 'getDatasetFromPath').mockReturnValue(null);

      const mockWatcher = {
        on: jest.fn().mockReturnThis(),
      };
      (chokidar.watch as jest.Mock).mockReturnValue(mockWatcher);

      service.start();

      // Get the ignored function from the call
      const callArgs = (chokidar.watch as jest.Mock).mock.calls[0];
      const options = callArgs[1];
      const ignoredFunction = options.ignored;

      // Test the ignored function - should ignore files not in any dataset
      expect(ignoredFunction('/some/other/path/movie.mkv')).toBe(true);
    });
  });

  describe('handleFileAdded', () => {
    it('should skip processing files with extensions not in dataset exts', () => {
      const mockConfig = {
        movies: {
          enabled: true,
          '/path/to/movies': {
            exts: ['mkv', 'mp4'],
          },
        },
      };

      datasetsService.getDatasetConfig.mockReturnValue(mockConfig);

      // Mock the getDatasetFromPath method
      jest
        .spyOn(service as any, 'getDatasetFromPath')
        .mockReturnValue('movies');

      // Spy on logger to check if debug message is logged
      const loggerSpy = jest.spyOn(service['logger'], 'debug');

      // Call handleFileAdded with a .nfo file
      (service as any).handleFileAdded('/path/to/movies/movie.nfo');

      // Should log debug message and return early
      expect(loggerSpy).toHaveBeenCalledWith(
        'Skipping file /path/to/movies/movie.nfo - extension not in dataset exts array',
      );
    });

    it('should process files with extensions in dataset exts', () => {
      const mockConfig = {
        movies: {
          enabled: true,
          '/path/to/movies': {
            exts: ['mkv', 'mp4'],
          },
        },
      };

      datasetsService.getDatasetConfig.mockReturnValue(mockConfig);

      // Mock the getDatasetFromPath method
      jest
        .spyOn(service as any, 'getDatasetFromPath')
        .mockReturnValue('movies');

      // Mock the validation worker
      const postMessageSpy = jest.fn();
      (service as any).validationWorker.postMessage = postMessageSpy;

      // Call handleFileAdded with a .mkv file
      (service as any).handleFileAdded('/path/to/movies/movie.mkv');

      // Should call validation worker
      expect(postMessageSpy).toHaveBeenCalledWith({
        type: 'validate_file',
        file: '/path/to/movies/movie.mkv',
      });
    });
  });
});
