import * as fse from 'fs-extra';
import temp from 'temp';

import { ILogServiceManager } from '@opensumi/ide-core-common/lib/log';
import { FileUri, sleep } from '@opensumi/ide-core-node';
import { createNodeInjector } from '@opensumi/ide-dev-tool/src/mock-injector';

import { DidFilesChangedParams, FileChangeType } from '../../src/common/index';
import { UnRecursiveFileSystemWatcher } from '../../src/node/hosted/un-recursive/file-service-watcher';

const sleepTime = 1000;

describe('unRecursively watch for folder additions, deletions, rename,and updates', () => {
  const track = temp.track();
  async function generateWatcher() {
    const injector = createNodeInjector([]);
    const root = FileUri.create(fse.realpathSync(await temp.mkdir('unRecursive-test')));
    const watcherServer = new UnRecursiveFileSystemWatcher(injector.get(ILogServiceManager).getLogger());
    fse.mkdirpSync(FileUri.fsPath(root.resolve('for_rename_folder')));
    fse.writeFileSync(FileUri.fsPath(root.resolve('for_rename')), 'rename');
    await watcherServer.watchFileChanges(root.path.toString());
    return { root, watcherServer };
  }
  const watcherServerList: UnRecursiveFileSystemWatcher[] = [];
  afterAll(async () => {
    track.cleanupSync();
    watcherServerList.forEach((watcherServer) => {
      watcherServer.dispose();
    });
  });
  it('Rename the files under the folder', async () => {
    const addUris = new Set<string>();
    const deleteUris = new Set<string>();

    const watcherClient = {
      onDidFilesChanged(event: DidFilesChangedParams) {
        event.changes.forEach((c) => {
          if (c.type === FileChangeType.ADDED) {
            addUris.add(c.uri);
          }
          if (c.type === FileChangeType.DELETED) {
            deleteUris.add(c.uri);
          }
        });
      },
    };
    const { root, watcherServer } = await generateWatcher();
    watcherServer.setClient(watcherClient);

    const expectedAddUris = [root.resolve('for_rename_renamed').toString()];

    const expectedDeleteUris = [root.resolve('for_rename').toString()];

    fse.renameSync(FileUri.fsPath(root.resolve('for_rename')), FileUri.fsPath(root.resolve('for_rename_renamed')));
    await sleep(sleepTime);
    expect([...addUris]).toEqual(expectedAddUris);
    expect([...deleteUris]).toEqual(expectedDeleteUris);
    watcherServerList.push(watcherServer);
  });
  it('Add the files under the folder', async () => {
    const addUris = new Set<string>();
    const deleteUris = new Set<string>();

    const watcherClient = {
      onDidFilesChanged(event: DidFilesChangedParams) {
        event.changes.forEach((c) => {
          if (c.type === FileChangeType.ADDED) {
            addUris.add(c.uri);
          }
          if (c.type === FileChangeType.DELETED) {
            deleteUris.add(c.uri);
          }
        });
      },
    };
    const { root, watcherServer } = await generateWatcher();
    watcherServer.setClient(watcherClient);

    const expectedAddUris = [root.resolve('README.md').toString()];

    const expectedDeleteUris = [];

    await fse.ensureFile(root.resolve('README.md').codeUri.fsPath.toString());
    await sleep(sleepTime);

    expect(Array.from(addUris)).toEqual(expectedAddUris);
    expect(Array.from(deleteUris)).toEqual(expectedDeleteUris);
    watcherServerList.push(watcherServer);
  });
  it('Update the files under the folder', async () => {
    const updatedUris = new Set<string>();
    const deleteUris = new Set<string>();
    const watcherClient = {
      onDidFilesChanged(event: DidFilesChangedParams) {
        event.changes.forEach((c) => {
          if (c.type === FileChangeType.UPDATED) {
            updatedUris.add(c.uri);
          }
          if (c.type === FileChangeType.DELETED) {
            deleteUris.add(c.uri);
          }
        });
      },
    };
    const { root, watcherServer } = await generateWatcher();
    watcherServer.setClient(watcherClient);
    const expectedDeleteUris = [];
    const expectedUpdatedUris = [root.resolve('for_rename').toString()];
    fse.writeFileSync(root.resolve('for_rename').codeUri.fsPath.toString(), '');
    await sleep(sleepTime);
    expect(Array.from(updatedUris)).toEqual(expectedUpdatedUris);
    expect(Array.from(deleteUris)).toEqual(expectedDeleteUris);
    watcherServerList.push(watcherServer);
  });
  it('Delete the files under the folder', async () => {
    const addUris = new Set<string>();
    const deleteUris = new Set<string>();

    const watcherClient = {
      onDidFilesChanged(event: DidFilesChangedParams) {
        event.changes.forEach((c) => {
          if (c.type === FileChangeType.ADDED) {
            addUris.add(c.uri);
          }
          if (c.type === FileChangeType.DELETED) {
            deleteUris.add(c.uri);
          }
        });
      },
    };
    const { root, watcherServer } = await generateWatcher();
    watcherServer.setClient(watcherClient);
    const expectedDeleteUris = [root.resolve('for_rename').toString()];
    const expectedAddUris = [];
    await fse.unlink(root.resolve('for_rename').codeUri.fsPath.toString());
    await sleep(sleepTime);

    expect(Array.from(addUris)).toEqual(expectedAddUris);
    expect(Array.from(deleteUris)).toEqual(expectedDeleteUris);
    watcherServerList.push(watcherServer);
  });
  it('Rename the watched folder', async () => {
    const addUris = new Set<string>();
    const deleteUris = new Set<string>();

    const watcherClient = {
      onDidFilesChanged(event: DidFilesChangedParams) {
        event.changes.forEach((c) => {
          if (c.type === FileChangeType.ADDED) {
            addUris.add(c.uri);
          }
          if (c.type === FileChangeType.DELETED) {
            deleteUris.add(c.uri);
          }
        });
      },
    };
    const { root, watcherServer } = await generateWatcher();
    watcherServer.setClient(watcherClient);

    const expectedAddUris = [];
    const expectedDeleteUris = [];

    fse.renameSync(
      FileUri.fsPath(root.resolve('for_rename_folder')),
      FileUri.fsPath(root.resolve('for_rename_folder_ed')),
    );
    await sleep(sleepTime);

    expect([...addUris]).toEqual(expectedAddUris);
    expect([...deleteUris]).toEqual(expectedDeleteUris);
    watcherServerList.push(watcherServer);
  });
  it('Add the watched folder', async () => {
    const addUris = new Set<string>();
    const deleteUris = new Set<string>();
    const watcherClient = {
      onDidFilesChanged(event: DidFilesChangedParams) {
        event.changes.forEach((c) => {
          if (c.type === FileChangeType.ADDED) {
            addUris.add(c.uri);
          }
          if (c.type === FileChangeType.DELETED) {
            deleteUris.add(c.uri);
          }
        });
      },
    };
    const { root, watcherServer } = await generateWatcher();
    watcherServer.setClient(watcherClient);

    const expectedAddUris = [];

    const expectedDeleteUris = [];

    await fse.ensureDir(root.resolve('README').codeUri.fsPath.toString());
    await sleep(sleepTime);

    expect(Array.from(addUris)).toEqual(expectedAddUris);
    expect(Array.from(deleteUris)).toEqual(expectedDeleteUris);
    watcherServerList.push(watcherServer);
  });

  it('Delete the watched folder', async () => {
    const addUris = new Set<string>();
    const deleteUris = new Set<string>();

    const watcherClient = {
      onDidFilesChanged(event: DidFilesChangedParams) {
        event.changes.forEach((c) => {
          if (c.type === FileChangeType.ADDED) {
            addUris.add(c.uri);
          }
          if (c.type === FileChangeType.DELETED) {
            deleteUris.add(c.uri);
          }
        });
      },
    };
    const { root, watcherServer } = await generateWatcher();
    watcherServer.setClient(watcherClient);
    const expectedDeleteUris = [];
    const expectedAddUris = [];
    await fse.remove(root.resolve('for_rename_folder').codeUri.fsPath.toString());
    await sleep(sleepTime);

    expect(Array.from(addUris)).toEqual(expectedAddUris);
    expect(Array.from(deleteUris)).toEqual(expectedDeleteUris);
    watcherServerList.push(watcherServer);
  });
});

describe('Delete and update monitored files', () => {
  const track = temp.track();
  async function generateWatcher() {
    const injector = createNodeInjector([]);
    const root = FileUri.create(fse.realpathSync(await temp.mkdir('unRecursive-test')));
    const watcherServer = new UnRecursiveFileSystemWatcher(injector.get(ILogServiceManager).getLogger());
    fse.writeFileSync(FileUri.fsPath(root.resolve('for_rename')), 'rename');
    await watcherServer.watchFileChanges(root.toString() + '/for_rename');
    return { root, watcherServer };
  }
  const watcherServerList: UnRecursiveFileSystemWatcher[] = [];
  afterAll(async () => {
    track.cleanupSync();
    watcherServerList.forEach((watcherServer) => {
      watcherServer.dispose();
    });
  });

  it('Delete watched files', async () => {
    const addUris = new Set<string>();
    const deleteUris = new Set<string>();

    const watcherClient = {
      onDidFilesChanged(event: DidFilesChangedParams) {
        event.changes.forEach((c) => {
          if (c.type === FileChangeType.ADDED) {
            addUris.add(c.uri);
          }
          if (c.type === FileChangeType.DELETED) {
            deleteUris.add(c.uri);
          }
        });
      },
    };
    const { root, watcherServer } = await generateWatcher();
    watcherServer.setClient(watcherClient);

    const expectedDeleteUris = [root.resolve('for_rename').toString()];

    const expectedAddUris = [];
    await fse.unlink(root.resolve('for_rename').codeUri.fsPath.toString());
    await sleep(sleepTime);
    expect(Array.from(addUris)).toEqual(expectedAddUris);
    expect(Array.from(deleteUris)).toEqual(expectedDeleteUris);
    watcherServerList.push(watcherServer);
  });

  it('Update watched files', async () => {
    const updatedUris = new Set<string>();
    const deleteUris = new Set<string>();

    const watcherClient = {
      onDidFilesChanged(event: DidFilesChangedParams) {
        event.changes.forEach((c) => {
          if (c.type === FileChangeType.UPDATED) {
            updatedUris.add(c.uri);
          }
          if (c.type === FileChangeType.DELETED) {
            deleteUris.add(c.uri);
          }
        });
      },
    };
    const { root, watcherServer } = await generateWatcher();
    watcherServer.setClient(watcherClient);
    const expectedDeleteUris = [];
    const expectedUpdatedUris = [root.resolve('for_rename').toString()];
    await fse.writeFile(root.resolve('for_rename').codeUri.fsPath.toString(), 'for');
    await sleep(sleepTime);
    expect(Array.from(updatedUris)).toEqual(expectedUpdatedUris);
    expect(Array.from(deleteUris)).toEqual(expectedDeleteUris);
    watcherServerList.push(watcherServer);
  });
});

describe('UnRecursiveFileSystemWatcher resource management', () => {
  const track = temp.track();

  async function generateWatcher() {
    const injector = createNodeInjector([]);
    const root = FileUri.create(fse.realpathSync(await temp.mkdir('unRecursive-resource-test')));
    const watcherServer = new UnRecursiveFileSystemWatcher(injector.get(ILogServiceManager).getLogger());
    fse.writeFileSync(FileUri.fsPath(root.resolve('test_file')), 'content');
    return { root, watcherServer };
  }

  afterAll(async () => {
    track.cleanupSync();
  });

  it('should add watcher to collections when watching', async () => {
    const { root, watcherServer } = await generateWatcher();

    await watcherServer.watchFileChanges(root.path.toString());

    // Access private property for testing
    const collections = watcherServer['watcherCollections'] as Map<string, fse.FSWatcher>;
    expect(collections.size).toBeGreaterThan(0);

    watcherServer.dispose();
  });

  it('should clear watcherCollections on dispose', async () => {
    const { root, watcherServer } = await generateWatcher();

    await watcherServer.watchFileChanges(root.path.toString());

    const collections = watcherServer['watcherCollections'] as Map<string, fse.FSWatcher>;
    expect(collections.size).toBeGreaterThan(0);

    watcherServer.dispose();

    expect(collections.size).toBe(0);
  });

  it('should remove watcher from collections on unwatchFileChanges', async () => {
    const { root, watcherServer } = await generateWatcher();

    await watcherServer.watchFileChanges(root.path.toString());

    const collections = watcherServer['watcherCollections'] as Map<string, fse.FSWatcher>;
    const sizeBefore = collections.size;
    expect(sizeBefore).toBeGreaterThan(0);

    watcherServer.unwatchFileChanges(root.path.toString());

    expect(collections.size).toBeLessThan(sizeBefore);

    watcherServer.dispose();
  });

  it('should handle symlink paths correctly in unwatchFileChanges', async () => {
    const { root, watcherServer } = await generateWatcher();

    // Create a symlink
    const realDir = FileUri.fsPath(root.resolve('real_dir'));
    const symlinkDir = FileUri.fsPath(root.resolve('symlink_dir'));

    fse.mkdirSync(realDir);
    fse.writeFileSync(`${realDir}/file.txt`, 'content');

    try {
      fse.symlinkSync(realDir, symlinkDir);
    } catch {
      // Skip test if symlinks are not supported
      watcherServer.dispose();
      return;
    }

    // Watch using symlink path
    await watcherServer.watchFileChanges(symlinkDir);

    const collections = watcherServer['watcherCollections'] as Map<string, fse.FSWatcher>;
    expect(collections.size).toBeGreaterThan(0);

    // Unwatch using symlink path - should still work
    watcherServer.unwatchFileChanges(symlinkDir);

    watcherServer.dispose();
  });

  it('should not trigger callbacks after dispose', async () => {
    const { root, watcherServer } = await generateWatcher();
    const receivedEvents: DidFilesChangedParams[] = [];

    const watcherClient = {
      onDidFilesChanged(event: DidFilesChangedParams) {
        receivedEvents.push(event);
      },
    };

    await watcherServer.watchFileChanges(root.path.toString());
    watcherServer.setClient(watcherClient);

    // Dispose immediately
    watcherServer.dispose();

    // Make file changes after dispose
    fse.writeFileSync(FileUri.fsPath(root.resolve('new_file')), 'new content');

    // Wait for potential delayed callbacks (FILE_DELETE_HANDLER_DELAY is 500ms)
    await sleep(1000);

    // Should not receive any events after dispose
    expect(receivedEvents.length).toBe(0);
  });

  it('should properly clean up via disposable from start method', async () => {
    const { root, watcherServer } = await generateWatcher();

    await watcherServer.watchFileChanges(root.path.toString());

    const collections = watcherServer['watcherCollections'] as Map<string, fse.FSWatcher>;
    expect(collections.size).toBeGreaterThan(0);

    // Dispose via toDispose (which contains the disposable from start)
    const toDispose = watcherServer['toDispose'];
    toDispose.dispose();

    // Collections should be cleared
    expect(collections.size).toBe(0);
  });

  it('should not watch duplicate paths', async () => {
    const { root, watcherServer } = await generateWatcher();

    await watcherServer.watchFileChanges(root.path.toString());
    const collections = watcherServer['watcherCollections'] as Map<string, fse.FSWatcher>;
    const sizeAfterFirst = collections.size;

    // Watch the same path again
    await watcherServer.watchFileChanges(root.path.toString());
    const sizeAfterSecond = collections.size;

    // Should not create duplicate watchers
    expect(sizeAfterSecond).toBe(sizeAfterFirst);

    watcherServer.dispose();
  });
});
