import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { TerminalService, getTerminalService } from '@/services/terminal-service.js';
import * as pty from 'node-pty';
import * as os from 'os';
import * as fs from 'fs';

vi.mock('node-pty');
vi.mock('fs');
vi.mock('os');

describe('terminal-service.ts', () => {
  let service: TerminalService;
  let mockPtyProcess: any;

  beforeEach(() => {
    vi.clearAllMocks();
    service = new TerminalService();

    // Mock PTY process
    mockPtyProcess = {
      onData: vi.fn(),
      onExit: vi.fn(),
      write: vi.fn(),
      resize: vi.fn(),
      kill: vi.fn(),
    };

    vi.mocked(pty.spawn).mockReturnValue(mockPtyProcess);
    vi.mocked(os.homedir).mockReturnValue('/home/user');
    vi.mocked(os.platform).mockReturnValue('linux');
    vi.mocked(os.arch).mockReturnValue('x64');
  });

  afterEach(() => {
    service.cleanup();
  });

  describe('detectShell', () => {
    it('should detect PowerShell Core on Windows when available', () => {
      vi.mocked(os.platform).mockReturnValue('win32');
      vi.mocked(fs.existsSync).mockImplementation((path: any) => {
        return path === 'C:\\Program Files\\PowerShell\\7\\pwsh.exe';
      });

      const result = service.detectShell();

      expect(result.shell).toBe('C:\\Program Files\\PowerShell\\7\\pwsh.exe');
      expect(result.args).toEqual([]);
    });

    it('should fall back to PowerShell on Windows if Core not available', () => {
      vi.mocked(os.platform).mockReturnValue('win32');
      vi.mocked(fs.existsSync).mockImplementation((path: any) => {
        return path === 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
      });

      const result = service.detectShell();

      expect(result.shell).toBe('C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe');
      expect(result.args).toEqual([]);
    });

    it('should fall back to cmd.exe on Windows if no PowerShell', () => {
      vi.mocked(os.platform).mockReturnValue('win32');
      vi.mocked(fs.existsSync).mockReturnValue(false);

      const result = service.detectShell();

      expect(result.shell).toBe('cmd.exe');
      expect(result.args).toEqual([]);
    });

    it('should detect user shell on macOS', () => {
      vi.mocked(os.platform).mockReturnValue('darwin');
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/zsh' });
      vi.mocked(fs.existsSync).mockReturnValue(true);

      const result = service.detectShell();

      expect(result.shell).toBe('/bin/zsh');
      expect(result.args).toEqual(['--login']);
    });

    it('should fall back to zsh on macOS if user shell not available', () => {
      vi.mocked(os.platform).mockReturnValue('darwin');
      vi.spyOn(process, 'env', 'get').mockReturnValue({});
      vi.mocked(fs.existsSync).mockImplementation((path: any) => {
        return path === '/bin/zsh';
      });

      const result = service.detectShell();

      expect(result.shell).toBe('/bin/zsh');
      expect(result.args).toEqual(['--login']);
    });

    it('should fall back to bash on macOS if zsh not available', () => {
      vi.mocked(os.platform).mockReturnValue('darwin');
      vi.spyOn(process, 'env', 'get').mockReturnValue({});
      vi.mocked(fs.existsSync).mockReturnValue(false);

      const result = service.detectShell();

      expect(result.shell).toBe('/bin/bash');
      expect(result.args).toEqual(['--login']);
    });

    it('should detect user shell on Linux', () => {
      vi.mocked(os.platform).mockReturnValue('linux');
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
      vi.mocked(fs.existsSync).mockReturnValue(true);

      const result = service.detectShell();

      expect(result.shell).toBe('/bin/bash');
      expect(result.args).toEqual(['--login']);
    });

    it('should fall back to bash on Linux if user shell not available', () => {
      vi.mocked(os.platform).mockReturnValue('linux');
      vi.spyOn(process, 'env', 'get').mockReturnValue({});
      vi.mocked(fs.existsSync).mockImplementation((path: any) => {
        return path === '/bin/bash';
      });

      const result = service.detectShell();

      expect(result.shell).toBe('/bin/bash');
      expect(result.args).toEqual(['--login']);
    });

    it('should fall back to sh on Linux if bash not available', () => {
      vi.mocked(os.platform).mockReturnValue('linux');
      vi.spyOn(process, 'env', 'get').mockReturnValue({});
      vi.mocked(fs.existsSync).mockReturnValue(false);

      const result = service.detectShell();

      expect(result.shell).toBe('/bin/sh');
      expect(result.args).toEqual([]);
    });

    it('should detect WSL and use appropriate shell', () => {
      vi.mocked(os.platform).mockReturnValue('linux');
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.readFileSync).mockReturnValue('Linux version 5.10.0-microsoft-standard-WSL2');

      const result = service.detectShell();

      expect(result.shell).toBe('/bin/bash');
      expect(result.args).toEqual(['--login']);
    });
  });

  describe('isWSL', () => {
    it('should return true if /proc/version contains microsoft', () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.readFileSync).mockReturnValue('Linux version 5.10.0-microsoft-standard-WSL2');

      expect(service.isWSL()).toBe(true);
    });

    it('should return true if /proc/version contains wsl', () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.readFileSync).mockReturnValue('Linux version 5.10.0-wsl2');

      expect(service.isWSL()).toBe(true);
    });

    it('should return true if WSL_DISTRO_NAME is set', () => {
      vi.mocked(fs.existsSync).mockReturnValue(false);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ WSL_DISTRO_NAME: 'Ubuntu' });

      expect(service.isWSL()).toBe(true);
    });

    it('should return true if WSLENV is set', () => {
      vi.mocked(fs.existsSync).mockReturnValue(false);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ WSLENV: 'PATH/l' });

      expect(service.isWSL()).toBe(true);
    });

    it('should return false if not in WSL', () => {
      vi.mocked(fs.existsSync).mockReturnValue(false);
      vi.spyOn(process, 'env', 'get').mockReturnValue({});

      expect(service.isWSL()).toBe(false);
    });

    it('should return false if error reading /proc/version', () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.readFileSync).mockImplementation(() => {
        throw new Error('Permission denied');
      });

      expect(service.isWSL()).toBe(false);
    });
  });

  describe('getPlatformInfo', () => {
    it('should return platform information', () => {
      vi.mocked(os.platform).mockReturnValue('linux');
      vi.mocked(os.arch).mockReturnValue('x64');
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });

      const info = service.getPlatformInfo();

      expect(info.platform).toBe('linux');
      expect(info.arch).toBe('x64');
      expect(info.defaultShell).toBe('/bin/bash');
      expect(typeof info.isWSL).toBe('boolean');
    });
  });

  describe('createSession', () => {
    it('should create a new terminal session', () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });

      const session = service.createSession({
        cwd: '/test/dir',
        cols: 100,
        rows: 30,
      });

      expect(session.id).toMatch(/^term-/);
      expect(session.cwd).toBe('/test/dir');
      expect(session.shell).toBe('/bin/bash');
      expect(pty.spawn).toHaveBeenCalledWith(
        '/bin/bash',
        ['--login'],
        expect.objectContaining({
          cwd: '/test/dir',
          cols: 100,
          rows: 30,
        })
      );
    });

    it('should use default cols and rows if not provided', () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });

      service.createSession();

      expect(pty.spawn).toHaveBeenCalledWith(
        expect.any(String),
        expect.any(Array),
        expect.objectContaining({
          cols: 80,
          rows: 24,
        })
      );
    });

    it('should fall back to home directory if cwd does not exist', () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.statSync).mockImplementation(() => {
        throw new Error('ENOENT');
      });
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });

      const session = service.createSession({
        cwd: '/nonexistent',
      });

      expect(session.cwd).toBe('/home/user');
    });

    it('should fall back to home directory if cwd is not a directory', () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => false } as any);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });

      const session = service.createSession({
        cwd: '/file.txt',
      });

      expect(session.cwd).toBe('/home/user');
    });

    it('should fix double slashes in path', () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });

      const session = service.createSession({
        cwd: '//test/dir',
      });

      expect(session.cwd).toBe('/test/dir');
    });

    it('should preserve WSL UNC paths', () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });

      const session = service.createSession({
        cwd: '//wsl$/Ubuntu/home',
      });

      expect(session.cwd).toBe('//wsl$/Ubuntu/home');
    });

    it('should handle data events from PTY', () => {
      vi.useFakeTimers();
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });

      const dataCallback = vi.fn();
      service.onData(dataCallback);

      service.createSession();

      // Simulate data event
      const onDataHandler = mockPtyProcess.onData.mock.calls[0][0];
      onDataHandler('test data');

      // Wait for throttled output
      vi.advanceTimersByTime(20);

      expect(dataCallback).toHaveBeenCalled();

      vi.useRealTimers();
    });

    it('should handle exit events from PTY', () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });

      const exitCallback = vi.fn();
      service.onExit(exitCallback);

      const session = service.createSession();

      // Simulate exit event
      const onExitHandler = mockPtyProcess.onExit.mock.calls[0][0];
      onExitHandler({ exitCode: 0 });

      expect(exitCallback).toHaveBeenCalledWith(session.id, 0);
      expect(service.getSession(session.id)).toBeUndefined();
    });
  });

  describe('write', () => {
    it('should write data to existing session', () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });

      const session = service.createSession();
      const result = service.write(session.id, 'ls\n');

      expect(result).toBe(true);
      expect(mockPtyProcess.write).toHaveBeenCalledWith('ls\n');
    });

    it('should return false for non-existent session', () => {
      const result = service.write('nonexistent', 'data');

      expect(result).toBe(false);
      expect(mockPtyProcess.write).not.toHaveBeenCalled();
    });
  });

  describe('resize', () => {
    it('should resize existing session', () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });

      const session = service.createSession();
      const result = service.resize(session.id, 120, 40);

      expect(result).toBe(true);
      expect(mockPtyProcess.resize).toHaveBeenCalledWith(120, 40);
    });

    it('should return false for non-existent session', () => {
      const result = service.resize('nonexistent', 120, 40);

      expect(result).toBe(false);
      expect(mockPtyProcess.resize).not.toHaveBeenCalled();
    });

    it('should handle resize errors', () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
      mockPtyProcess.resize.mockImplementation(() => {
        throw new Error('Resize failed');
      });

      const session = service.createSession();
      const result = service.resize(session.id, 120, 40);

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

  describe('killSession', () => {
    it('should kill existing session', () => {
      vi.useFakeTimers();
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });

      const session = service.createSession();
      const result = service.killSession(session.id);

      expect(result).toBe(true);
      expect(mockPtyProcess.kill).toHaveBeenCalledWith('SIGTERM');

      // Session is removed after SIGKILL timeout (1 second)
      vi.advanceTimersByTime(1000);

      expect(mockPtyProcess.kill).toHaveBeenCalledWith('SIGKILL');
      expect(service.getSession(session.id)).toBeUndefined();

      vi.useRealTimers();
    });

    it('should return false for non-existent session', () => {
      const result = service.killSession('nonexistent');

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

    it('should handle kill errors', () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
      mockPtyProcess.kill.mockImplementation(() => {
        throw new Error('Kill failed');
      });

      const session = service.createSession();
      const result = service.killSession(session.id);

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

  describe('getSession', () => {
    it('should return existing session', () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });

      const session = service.createSession();
      const retrieved = service.getSession(session.id);

      expect(retrieved).toBe(session);
    });

    it('should return undefined for non-existent session', () => {
      const retrieved = service.getSession('nonexistent');

      expect(retrieved).toBeUndefined();
    });
  });

  describe('getScrollback', () => {
    it('should return scrollback buffer for existing session', () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });

      const session = service.createSession();
      session.scrollbackBuffer = 'test scrollback';

      const scrollback = service.getScrollback(session.id);

      expect(scrollback).toBe('test scrollback');
    });

    it('should return null for non-existent session', () => {
      const scrollback = service.getScrollback('nonexistent');

      expect(scrollback).toBeNull();
    });
  });

  describe('getAllSessions', () => {
    it('should return all active sessions', () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });

      const session1 = service.createSession({ cwd: '/dir1' });
      const session2 = service.createSession({ cwd: '/dir2' });

      const sessions = service.getAllSessions();

      expect(sessions).toHaveLength(2);
      expect(sessions[0].id).toBe(session1.id);
      expect(sessions[1].id).toBe(session2.id);
      expect(sessions[0].cwd).toBe('/dir1');
      expect(sessions[1].cwd).toBe('/dir2');
    });

    it('should return empty array if no sessions', () => {
      const sessions = service.getAllSessions();

      expect(sessions).toEqual([]);
    });
  });

  describe('onData and onExit', () => {
    it('should allow subscribing and unsubscribing from data events', () => {
      const callback = vi.fn();
      const unsubscribe = service.onData(callback);

      expect(typeof unsubscribe).toBe('function');

      unsubscribe();
    });

    it('should allow subscribing and unsubscribing from exit events', () => {
      const callback = vi.fn();
      const unsubscribe = service.onExit(callback);

      expect(typeof unsubscribe).toBe('function');

      unsubscribe();
    });
  });

  describe('cleanup', () => {
    it('should clean up all sessions', () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });

      const session1 = service.createSession();
      const session2 = service.createSession();

      service.cleanup();

      expect(service.getSession(session1.id)).toBeUndefined();
      expect(service.getSession(session2.id)).toBeUndefined();
      expect(service.getAllSessions()).toHaveLength(0);
    });

    it('should handle cleanup errors gracefully', () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.statSync).mockReturnValue({ isDirectory: () => true } as any);
      vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
      mockPtyProcess.kill.mockImplementation(() => {
        throw new Error('Kill failed');
      });

      service.createSession();

      expect(() => service.cleanup()).not.toThrow();
    });
  });

  describe('getTerminalService', () => {
    it('should return singleton instance', () => {
      const instance1 = getTerminalService();
      const instance2 = getTerminalService();

      expect(instance1).toBe(instance2);
    });
  });
});
