/**
 * Cursor Routes Tests
 * Endpoint contract tests without module-level mocks.
 */

import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'bun:test';
import express from 'express';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import type { Server } from 'http';
import { execFileSync, spawnSync } from 'child_process';

let server: Server;
let baseUrl = '';
let tempDir = '';
let originalCcsHome: string | undefined;

let setGlobalConfigDir: (dir: string | undefined) => void;
let getCcsDir: () => string;
let loadOrCreateUnifiedConfig: () => {
  cursor?: {
    enabled?: boolean;
    port?: number;
    auto_start?: boolean;
    ghost_mode?: boolean;
    model?: string;
  };
};
let saveUnifiedConfig: (config: {
  cursor?: {
    enabled?: boolean;
    port?: number;
    auto_start?: boolean;
    ghost_mode?: boolean;
    model?: string;
  };
}) => void;
let saveCredentials: (credentials: {
  accessToken: string;
  machineId: string;
  authMethod: 'manual' | 'auto-detect';
  importedAt: string;
}) => void;
let deleteCredentials: () => boolean;
let checkAuthStatus: () => {
  authenticated: boolean;
  expired?: boolean;
  credentials?: {
    authMethod?: 'manual' | 'auto-detect';
    machineId?: string;
  };
};
let getTokenStoragePath: () => string;
let getDaemonStartPreconditionError: (
  input: { enabled: boolean; authenticated: boolean; tokenExpired?: boolean }
) => { status: number; error: string } | null;
let getAutoDetectFailureStatus: (
  reason?:
    | 'db_not_found'
    | 'sqlite_unavailable'
    | 'db_query_failed'
    | 'access_token_not_found'
    | 'machine_id_not_found'
    | 'invalid_token_format'
) => number;

function seedCursorConfig(overrides: {
  enabled?: boolean;
  port?: number;
  auto_start?: boolean;
  ghost_mode?: boolean;
  model?: string;
} = {}): void {
  const config = loadOrCreateUnifiedConfig();
  config.cursor = {
    enabled: overrides.enabled ?? true,
    port: overrides.port ?? 20129,
    auto_start: overrides.auto_start ?? false,
    ghost_mode: overrides.ghost_mode ?? true,
    model: overrides.model ?? 'gpt-5.3-codex',
  };
  saveUnifiedConfig(config);
}

function seedCredentials(expired: boolean): void {
  saveCredentials({
    accessToken: 'a'.repeat(60),
    machineId: '1234567890abcdef1234567890abcdef',
    authMethod: 'manual',
    importedAt: expired
      ? new Date(Date.now() - 26 * 60 * 60 * 1000).toISOString()
      : new Date().toISOString(),
  });
}

beforeAll(async () => {
  originalCcsHome = process.env.CCS_HOME;
  tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-cursor-routes-test-'));
  process.env.CCS_HOME = tempDir;

  const configManager = await import('../../../src/utils/config-manager');
  setGlobalConfigDir = configManager.setGlobalConfigDir;
  getCcsDir = configManager.getCcsDir;
  setGlobalConfigDir(undefined);

  const unifiedConfig = await import('../../../src/config/unified-config-loader');
  loadOrCreateUnifiedConfig = unifiedConfig.loadOrCreateUnifiedConfig;
  saveUnifiedConfig = unifiedConfig.saveUnifiedConfig;

  const cursorAuth = await import('../../../src/cursor/cursor-auth');
  saveCredentials = cursorAuth.saveCredentials;
  deleteCredentials = cursorAuth.deleteCredentials;
  checkAuthStatus = cursorAuth.checkAuthStatus;
  getTokenStoragePath = cursorAuth.getTokenStoragePath;

  const cursorRoutesModule = await import('../../../src/web-server/routes/cursor-routes');
  getDaemonStartPreconditionError = cursorRoutesModule.getDaemonStartPreconditionError;
  getAutoDetectFailureStatus = cursorRoutesModule.getAutoDetectFailureStatus;

  const app = express();
  app.use(express.json());
  app.use('/api/cursor', cursorRoutesModule.default);

  server = app.listen(0, '127.0.0.1');
  await new Promise<void>((resolve) => server.on('listening', () => resolve()));
  const address = server.address();
  if (!address || typeof address === 'string') {
    throw new Error('Unable to resolve test server port');
  }
  baseUrl = `http://127.0.0.1:${address.port}`;
});

beforeEach(() => {
  fs.rmSync(tempDir, { recursive: true, force: true });
  fs.mkdirSync(tempDir, { recursive: true });

  setGlobalConfigDir(undefined);
  const ccsDir = getCcsDir();
  if (!fs.existsSync(ccsDir)) {
    fs.mkdirSync(ccsDir, { recursive: true });
  }

  seedCursorConfig();

  // Ensure clean auth state for each test.
  deleteCredentials();
});

afterAll(async () => {
  if (server) {
    await new Promise<void>((resolve) => server.close(() => resolve()));
  }

  if (originalCcsHome !== undefined) {
    process.env.CCS_HOME = originalCcsHome;
  } else {
    delete process.env.CCS_HOME;
  }
  setGlobalConfigDir(undefined);

  if (tempDir && fs.existsSync(tempDir)) {
    fs.rmSync(tempDir, { recursive: true, force: true });
  }
});

describe('Cursor Routes Logic', () => {
  describe('POST /daemon/start preconditions', () => {
    it('blocks start when integration is disabled', () => {
      const result = getDaemonStartPreconditionError({
        enabled: false,
        authenticated: true,
        tokenExpired: false,
      });

      expect(result).toEqual({
        status: 400,
        error: 'Cursor integration is disabled. Enable it before starting daemon.',
      });
    });

    it('blocks start when not authenticated', () => {
      const result = getDaemonStartPreconditionError({
        enabled: true,
        authenticated: false,
        tokenExpired: false,
      });

      expect(result).toEqual({
        status: 401,
        error: 'Cursor authentication required. Import credentials before starting daemon.',
      });
    });

    it('blocks start when token is expired', () => {
      const result = getDaemonStartPreconditionError({
        enabled: true,
        authenticated: true,
        tokenExpired: true,
      });

      expect(result).toEqual({
        status: 401,
        error: 'Cursor credentials expired. Re-authenticate before starting daemon.',
      });
    });

    it('allows start when all preconditions are met', () => {
      const result = getDaemonStartPreconditionError({
        enabled: true,
        authenticated: true,
        tokenExpired: false,
      });

      expect(result).toBeNull();
    });
  });

  describe('HTTP contracts', () => {
    it('maps invalid_token_format auto-detect failures to HTTP 400', () => {
      expect(getAutoDetectFailureStatus('invalid_token_format')).toBe(400);
    });

    it('GET /api/cursor/status returns current state', async () => {
      const res = await fetch(`${baseUrl}/api/cursor/status`);
      expect(res.status).toBe(200);

      const json = (await res.json()) as {
        enabled: boolean;
        authenticated: boolean;
        token_expired: boolean;
        daemon_running: boolean;
        port: number;
      };

      expect(json.enabled).toBe(true);
      expect(json.authenticated).toBe(false);
      expect(json.token_expired).toBe(false);
      expect(json.daemon_running).toBe(false);
      expect(json.port).toBe(20129);
    });

    it('POST /api/cursor/auth/import validates required fields', async () => {
      const res = await fetch(`${baseUrl}/api/cursor/auth/import`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ accessToken: 'only-token' }),
      });

      expect(res.status).toBe(400);
      const json = (await res.json()) as { error?: string };
      expect(json.error).toContain('Missing accessToken or machineId');
    });

    it('POST /api/cursor/auth/import rejects invalid token format', async () => {
      const res = await fetch(`${baseUrl}/api/cursor/auth/import`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          accessToken: 'short',
          machineId: 'bad',
        }),
      });

      expect(res.status).toBe(400);
      const json = (await res.json()) as { error?: string };
      expect(json.error).toContain('Invalid token or machine ID format');
      expect(checkAuthStatus().authenticated).toBe(false);
    });

    it('POST /api/cursor/auth/import persists valid credentials', async () => {
      const res = await fetch(`${baseUrl}/api/cursor/auth/import`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          accessToken: 'a'.repeat(60),
          machineId: '1234567890abcdef1234567890abcdef',
        }),
      });

      expect(res.status).toBe(200);
      expect(checkAuthStatus().authenticated).toBe(true);
    });

    it('POST /api/cursor/auth/auto-detect returns 404 when no token source found', async () => {
      const originalHome = process.env.HOME;
      const isolatedHome = path.join(tempDir, 'auto-detect-empty-home');
      process.env.HOME = isolatedHome;
      fs.mkdirSync(isolatedHome, { recursive: true });

      try {
        const res = await fetch(`${baseUrl}/api/cursor/auth/auto-detect`, {
          method: 'POST',
        });

        expect(res.status).toBe(404);
        const json = (await res.json()) as { error?: string; reason?: string };
        expect(typeof json.error).toBe('string');
        expect(json.error?.length).toBeGreaterThan(0);
        expect(json.reason).toBe('db_not_found');
        expect(json.error).not.toContain(isolatedHome);
      } finally {
        if (originalHome !== undefined) {
          process.env.HOME = originalHome;
        } else {
          delete process.env.HOME;
        }
      }
    });

    it('POST /api/cursor/auth/auto-detect persists credentials on success', async () => {
      if (process.platform === 'win32') {
        return;
      }

      const sqliteCheck = spawnSync('sqlite3', ['--version'], { stdio: 'ignore' });
      if (sqliteCheck.status !== 0) {
        return;
      }

      const originalHome = process.env.HOME;
      const fakeHome = path.join(tempDir, 'fake-home');
      process.env.HOME = fakeHome;
      fs.mkdirSync(fakeHome, { recursive: true });

      const token = 'a'.repeat(60);
      const machineId = '1234567890abcdef1234567890abcdef';

      try {
        const dbPath = getTokenStoragePath();
        fs.mkdirSync(path.dirname(dbPath), { recursive: true });
        execFileSync('sqlite3', [dbPath, 'CREATE TABLE IF NOT EXISTS itemTable (key TEXT PRIMARY KEY, value TEXT);']);
        execFileSync('sqlite3', [
          dbPath,
          `INSERT OR REPLACE INTO itemTable (key, value) VALUES ('cursorAuth/accessToken', '${token}');`,
        ]);
        execFileSync('sqlite3', [
          dbPath,
          `INSERT OR REPLACE INTO itemTable (key, value) VALUES ('storage.serviceMachineId', '${machineId}');`,
        ]);

        const res = await fetch(`${baseUrl}/api/cursor/auth/auto-detect`, {
          method: 'POST',
        });

        expect(res.status).toBe(200);
        const json = (await res.json()) as { success?: boolean };
        expect(json.success).toBe(true);

        const auth = checkAuthStatus();
        expect(auth.authenticated).toBe(true);
        expect(auth.credentials?.authMethod).toBe('auto-detect');
        expect(auth.credentials?.machineId).toBe(machineId);
      } finally {
        if (originalHome !== undefined) {
          process.env.HOME = originalHome;
        } else {
          delete process.env.HOME;
        }
      }
    });

    it('POST /api/cursor/auth/auto-detect returns 503 when sqlite3 is unavailable', async () => {
      if (process.platform === 'win32') {
        return;
      }

      const originalHome = process.env.HOME;
      const originalPath = process.env.PATH;
      const originalSqliteBin = process.env.CCS_CURSOR_SQLITE_BIN;
      const fakeHome = path.join(tempDir, 'sqlite-missing-home');
      process.env.HOME = fakeHome;
      process.env.CCS_CURSOR_SQLITE_BIN = 'definitely-missing-sqlite3';

      try {
        const dbPath = getTokenStoragePath();
        fs.mkdirSync(path.dirname(dbPath), { recursive: true });
        fs.writeFileSync(dbPath, '');

        const res = await fetch(`${baseUrl}/api/cursor/auth/auto-detect`, {
          method: 'POST',
        });

        expect(res.status).toBe(503);
        const json = (await res.json()) as { reason?: string; db_path?: string };
        expect(json.reason).toBe('sqlite_unavailable');
        expect(json.db_path).toBeUndefined();
      } finally {
        if (originalHome !== undefined) {
          process.env.HOME = originalHome;
        } else {
          delete process.env.HOME;
        }

        if (originalPath !== undefined) {
          process.env.PATH = originalPath;
        } else {
          delete process.env.PATH;
        }
        if (originalSqliteBin !== undefined) {
          process.env.CCS_CURSOR_SQLITE_BIN = originalSqliteBin;
        } else {
          delete process.env.CCS_CURSOR_SQLITE_BIN;
        }
      }
    });

    it('POST /api/cursor/auth/auto-detect returns 500 when the database exists but cannot be queried', async () => {
      if (process.platform === 'win32') {
        return;
      }

      const sqliteCheck = spawnSync('sqlite3', ['--version'], { stdio: 'ignore' });
      if (sqliteCheck.status !== 0) {
        return;
      }

      const originalHome = process.env.HOME;
      const fakeHome = path.join(tempDir, 'query-failed-home');
      process.env.HOME = fakeHome;

      try {
        const dbPath = getTokenStoragePath();
        fs.mkdirSync(path.dirname(dbPath), { recursive: true });
        fs.writeFileSync(dbPath, 'not-a-sqlite-database');

        const res = await fetch(`${baseUrl}/api/cursor/auth/auto-detect`, {
          method: 'POST',
        });

        expect(res.status).toBe(500);
        const json = (await res.json()) as { reason?: string; db_path?: string };
        expect(json.reason).toBe('db_query_failed');
        expect(json.db_path).toBeUndefined();
      } finally {
        if (originalHome !== undefined) {
          process.env.HOME = originalHome;
        } else {
          delete process.env.HOME;
        }
      }
    });

    it('POST /api/cursor/daemon/start returns 400 when integration is disabled', async () => {
      seedCursorConfig({ enabled: false });
      seedCredentials(false);

      const res = await fetch(`${baseUrl}/api/cursor/daemon/start`, { method: 'POST' });
      expect(res.status).toBe(400);
      const json = (await res.json()) as { success?: boolean; error?: string };
      expect(json.success).toBe(false);
      expect(json.error).toContain('disabled');
    });

    it('POST /api/cursor/daemon/start returns 401 when unauthenticated', async () => {
      seedCursorConfig({ enabled: true });

      const res = await fetch(`${baseUrl}/api/cursor/daemon/start`, { method: 'POST' });
      expect(res.status).toBe(401);
      const json = (await res.json()) as { error?: string };
      expect(json.error).toContain('authentication required');
    });

    it('POST /api/cursor/daemon/start returns 401 when token is expired', async () => {
      seedCursorConfig({ enabled: true });
      seedCredentials(true);

      const res = await fetch(`${baseUrl}/api/cursor/daemon/start`, { method: 'POST' });
      expect(res.status).toBe(401);
      const json = (await res.json()) as { error?: string };
      expect(json.error).toContain('expired');
    });

    it(
      'POST /api/cursor/daemon/start starts daemon and /daemon/stop stops it',
      async () => {
        const port = 15000 + Math.floor(Math.random() * 20000);
        seedCursorConfig({ enabled: true, port });
        seedCredentials(false);

        const startRes = await fetch(`${baseUrl}/api/cursor/daemon/start`, { method: 'POST' });
        expect(startRes.status).toBe(200);

        const startJson = (await startRes.json()) as { success?: boolean; pid?: number };
        expect(startJson.success).toBe(true);
        expect(typeof startJson.pid).toBe('number');

        const stopRes = await fetch(`${baseUrl}/api/cursor/daemon/stop`, { method: 'POST' });
        expect(stopRes.status).toBe(200);
        const stopJson = (await stopRes.json()) as { success?: boolean };
        expect(stopJson.success).toBe(true);
      },
      35000
    );

    it('POST /api/cursor/daemon/stop returns success when daemon is not running', async () => {
      const res = await fetch(`${baseUrl}/api/cursor/daemon/stop`, { method: 'POST' });
      expect(res.status).toBe(200);
      const json = (await res.json()) as { success?: boolean };
      expect(json.success).toBe(true);
    });

    it('GET /api/cursor/models returns current model and list payload', async () => {
      const res = await fetch(`${baseUrl}/api/cursor/models`);
      expect(res.status).toBe(200);

      const json = (await res.json()) as { models: Array<{ id: string }>; current: string };
      expect(Array.isArray(json.models)).toBe(true);
      expect(json.models.length).toBeGreaterThan(0);
      expect(json.current).toBe('gpt-5.3-codex');
    });

    it('POST /api/cursor/probe returns auth failure when credentials are missing', async () => {
      const res = await fetch(`${baseUrl}/api/cursor/probe`, { method: 'POST' });
      expect(res.status).toBe(401);

      const json = (await res.json()) as { ok?: boolean; stage?: string; error_type?: string };
      expect(json.ok).toBe(false);
      expect(json.stage).toBe('auth');
      expect(json.error_type).toBe('authentication_error');
    });

    it('POST /api/cursor/probe returns daemon failure when daemon is down and auto_start is false', async () => {
      seedCursorConfig({ enabled: true, auto_start: false });
      seedCredentials(false);

      const res = await fetch(`${baseUrl}/api/cursor/probe`, { method: 'POST' });
      expect(res.status).toBe(503);

      const json = (await res.json()) as { ok?: boolean; stage?: string; error_type?: string };
      expect(json.ok).toBe(false);
      expect(json.stage).toBe('daemon');
      expect(json.error_type).toBe('daemon_not_running');
    });
  });
});
