import { beforeEach, describe, expect, it, vi } from 'vitest';
import { semverLt, formatUpdateNotice } from '../src/runtime/update-checker.js';
import type { UpdateCheckResult } from '../src/runtime/update-checker.js';

const { execSyncMock, cacheFiles } = vi.hoisted(() => ({
  execSyncMock: vi.fn(),
  cacheFiles: new Map<string, string>(),
}));

vi.mock('node:child_process', () => ({
  execSync: execSyncMock,
}));

vi.mock('node:fs', () => ({
  existsSync: vi.fn((path: string) => cacheFiles.has(path)),
  mkdirSync: vi.fn(),
  readFileSync: vi.fn((path: string) => {
    const value = cacheFiles.get(path);
    if (value === undefined) {
      throw new Error(`ENOENT: ${path}`);
    }
    return value;
  }),
  writeFileSync: vi.fn((path: string, content: string) => {
    cacheFiles.set(path, content);
  }),
}));

vi.mock('node:os', () => ({
  homedir: vi.fn(() => '/mock-home'),
}));

async function importUpdateChecker() {
  vi.resetModules();
  return import('../src/runtime/update-checker.js');
}

describe('semverLt', () => {
  it('returns true when a < b', () => {
    expect(semverLt('0.0.1', '0.0.2')).toBe(true);
    expect(semverLt('0.1.0', '0.2.0')).toBe(true);
    expect(semverLt('1.0.0', '2.0.0')).toBe(true);
    expect(semverLt('0.0.1-alpha.1', '0.0.1-alpha.2')).toBe(true);
    expect(semverLt('0.0.1-alpha.1', '0.0.1')).toBe(true);
  });

  it('returns false when a >= b', () => {
    expect(semverLt('0.0.2', '0.0.1')).toBe(false);
    expect(semverLt('1.0.0', '1.0.0')).toBe(false);
    expect(semverLt('2.0.0', '1.0.0')).toBe(false);
  });

  it('returns false for empty strings', () => {
    expect(semverLt('', '1.0.0')).toBe(false);
    expect(semverLt('1.0.0', '')).toBe(false);
    expect(semverLt('', '')).toBe(false);
  });
});

describe('formatUpdateNotice', () => {
  beforeEach(() => {
    execSyncMock.mockReset();
    cacheFiles.clear();
  });

  it('returns empty string when up to date', () => {
    const result: UpdateCheckResult = {
      current: '1.0.0',
      latest: '1.0.0',
      updateAvailable: false,
      checkedAt: new Date().toISOString(),
      registry: 'https://example.com',
    };
    expect(formatUpdateNotice(result)).toBe('');
  });

  it('returns a notice when update is available', () => {
    const result: UpdateCheckResult = {
      current: '0.0.1',
      latest: '0.1.0',
      updateAvailable: true,
      checkedAt: new Date().toISOString(),
      registry: 'https://example.com',
    };
    const notice = formatUpdateNotice(result);
    expect(notice).toContain('0.0.1');
    expect(notice).toContain('0.1.0');
    expect(notice).toContain('Update available');
  });

  it('uses @mosaicstack/mosaic for installs', async () => {
    execSyncMock.mockImplementation((command: string) => {
      if (command.includes('ls -g --depth=0 --json')) {
        return JSON.stringify({
          dependencies: {
            '@mosaicstack/mosaic': { version: '0.0.19' },
          },
        });
      }

      if (command.includes('view @mosaicstack/mosaic version')) {
        return '0.0.20';
      }

      throw new Error(`Unexpected command: ${command}`);
    });

    const { checkForUpdate } = await importUpdateChecker();
    const result = checkForUpdate({ skipCache: true });
    const notice = formatUpdateNotice(result);

    expect(result.current).toBe('0.0.19');
    expect(result.latest).toBe('0.0.20');
    expect(result.currentPackage).toBe('@mosaicstack/mosaic');
    expect(result.targetPackage).toBe('@mosaicstack/mosaic');
    expect(notice).toContain('@mosaicstack/mosaic@latest');
  });

  it('does not query legacy @mosaicstack/cli package', async () => {
    execSyncMock.mockImplementation((command: string) => {
      if (command.includes('view @mosaicstack/cli')) {
        throw new Error('Should not query @mosaicstack/cli');
      }

      if (command.includes('ls -g --depth=0 --json')) {
        return JSON.stringify({
          dependencies: {
            '@mosaicstack/mosaic': { version: '0.0.19' },
          },
        });
      }

      if (command.includes('view @mosaicstack/mosaic version')) {
        return '0.0.20';
      }

      throw new Error(`Unexpected command: ${command}`);
    });

    const { checkForUpdate } = await importUpdateChecker();
    const result = checkForUpdate({ skipCache: true });

    expect(result.targetPackage).toBe('@mosaicstack/mosaic');
    expect(result.latest).toBe('0.0.20');
    // Verify no @mosaicstack/cli queries were made
    const calls = execSyncMock.mock.calls.map((c: any[]) => c[0] as string);
    expect(calls.some((c) => c.includes('@mosaicstack/cli'))).toBe(false);
  });

  it('returns empty result when package is not installed', async () => {
    execSyncMock.mockImplementation((command: string) => {
      if (command.includes('ls -g --depth=0 --json')) {
        return JSON.stringify({ dependencies: {} });
      }

      if (command.includes('view @mosaicstack/mosaic version')) {
        return '';
      }

      throw new Error(`Unexpected command: ${command}`);
    });

    const { checkForUpdate } = await importUpdateChecker();
    const result = checkForUpdate({ skipCache: true });

    expect(result.current).toBe('');
    expect(result.updateAvailable).toBe(false);
  });
});
