/**
 * Tests for auto-generated OpenAPI spec.
 *
 * Verifies:
 *  1. Every server.js route appears in the spec (no drift)
 *  2. No stale routes in the spec that aren't in server.js
 *  3. Spec structure is valid OpenAPI 3.0.x
 *  4. info.version matches package.json
 *  5. Every operation has responses and tags
 *  6. Enriched routes have proper metadata
 */

import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import swaggerJsdoc from 'swagger-jsdoc';
import { swaggerDefinition } from '../../lib/openapi.js';

const __dirname = dirname(fileURLToPath(import.meta.url));
const serverPath = join(__dirname, '..', '..', 'server.js');
const serverSrc = readFileSync(serverPath, 'utf8');
const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8'));

// Build spec from JSDoc in server.js
const spec = swaggerJsdoc({
  definition: swaggerDefinition,
  apis: [serverPath],
});

/**
 * Extract all app.get/post/delete routes from server.js source.
 * Returns Set of "METHOD /path" strings (Express format with :params).
 */
function parseServerRoutes(source) {
  const routes = new Set();
  const re = /app\.(get|post|put|patch|delete)\(\s*['"`]([^'"`]+)['"`]/g;
  let m;
  while ((m = re.exec(source)) !== null) {
    routes.add(`${m[1].toUpperCase()} ${m[2]}`);
  }
  return routes;
}

const serverRoutes = parseServerRoutes(serverSrc);

describe('OpenAPI spec', () => {
  test('is valid OpenAPI 3.0.x shape', () => {
    expect(spec.openapi).toMatch(/^3\.0\.\d+$/);
    expect(spec.info).toBeDefined();
    expect(spec.info.title).toBe('camofox-browser');
    expect(spec.paths).toBeDefined();
    expect(typeof spec.paths).toBe('object');
    expect(spec.components).toBeDefined();
  });

  test('info.version matches package.json', () => {
    expect(spec.info.version).toBe(pkg.version);
  });

  test('every server.js route appears in the spec', () => {
    const missing = [];
    for (const route of serverRoutes) {
      const [method, expressPath] = route.split(' ');
      const oaPath = expressPath.replace(/:(\w+)/g, '{$1}');
      const pathObj = spec.paths[oaPath];
      if (!pathObj || !pathObj[method.toLowerCase()]) {
        missing.push(route);
      }
    }
    expect(missing).toEqual([]);
  });

  test('no stale routes in spec that are not in server.js', () => {
    const stale = [];
    for (const [oaPath, methods] of Object.entries(spec.paths)) {
      for (const method of Object.keys(methods)) {
        if (method.startsWith('x-')) continue; // skip extensions
        const expressPath = oaPath.replace(/\{(\w+)\}/g, ':$1');
        const key = `${method.toUpperCase()} ${expressPath}`;
        if (!serverRoutes.has(key)) {
          stale.push(key);
        }
      }
    }
    expect(stale).toEqual([]);
  });

  test('spec covers at least 30 routes from server.js', () => {
    expect(serverRoutes.size).toBeGreaterThanOrEqual(30);

    let covered = 0;
    for (const route of serverRoutes) {
      const [method, expressPath] = route.split(' ');
      const oaPath = expressPath.replace(/:(\w+)/g, '{$1}');
      if (spec.paths[oaPath]?.[method.toLowerCase()]) covered++;
    }
    expect(covered).toBe(serverRoutes.size);
  });

  test('every operation has at least one response', () => {
    const noResponses = [];
    for (const [path, methods] of Object.entries(spec.paths)) {
      for (const [method, op] of Object.entries(methods)) {
        if (method.startsWith('x-')) continue;
        if (!op.responses || Object.keys(op.responses).length === 0) {
          noResponses.push(`${method.toUpperCase()} ${path}`);
        }
      }
    }
    expect(noResponses).toEqual([]);
  });

  test('every operation has at least one tag', () => {
    for (const [path, methods] of Object.entries(spec.paths)) {
      for (const [method, op] of Object.entries(methods)) {
        if (method.startsWith('x-')) continue;
        expect(op.tags?.length).toBeGreaterThanOrEqual(1);
      }
    }
  });

  test('parameterized routes have path parameters', () => {
    const navOp = spec.paths['/tabs/{tabId}/navigate']?.post;
    expect(navOp).toBeDefined();
    const tabIdParam = navOp.parameters?.find(p => p.name === 'tabId' && p.in === 'path');
    expect(tabIdParam).toBeDefined();
    expect(tabIdParam.required).toBe(true);
    expect(tabIdParam.schema.type).toBe('string');
  });

  test('POST /tabs has request body and proper tag', () => {
    const createTab = spec.paths['/tabs']?.post;
    expect(createTab).toBeDefined();
    expect(createTab.summary).toBe('Create a new tab');
    expect(createTab.tags).toContain('Tabs');
    expect(createTab.requestBody).toBeDefined();
    expect(createTab.requestBody.content['application/json']).toBeDefined();
  });

  test('legacy routes are marked deprecated', () => {
    const legacyPaths = {
      '/act': 'post',
      '/navigate': 'post',
      '/snapshot': 'get',
      '/tabs/open': 'post',
    };
    for (const [path, method] of Object.entries(legacyPaths)) {
      const op = spec.paths[path]?.[method];
      expect(op).toBeDefined();
      expect(op.deprecated).toBe(true);
    }
  });

  test('Error schema is defined in components', () => {
    expect(spec.components.schemas.Error).toBeDefined();
    expect(spec.components.schemas.Error.required).toContain('error');
  });

  test('tags include well-known categories', () => {
    const tagNames = spec.tags.map(t => t.name);
    for (const expected of ['System', 'Tabs', 'Navigation', 'Interaction', 'Content', 'Sessions', 'Legacy', 'Browser']) {
      expect(tagNames).toContain(expected);
    }
  });

  test('security scheme BearerAuth is defined', () => {
    expect(spec.components.securitySchemes.BearerAuth).toBeDefined();
    expect(spec.components.securitySchemes.BearerAuth.type).toBe('http');
    expect(spec.components.securitySchemes.BearerAuth.scheme).toBe('bearer');
  });

  test('cookie import route has security requirement', () => {
    const op = spec.paths['/sessions/{userId}/cookies']?.post;
    expect(op).toBeDefined();
    expect(op.security).toEqual([{ BearerAuth: [] }]);
  });

  test('$ref references resolve to existing component schemas', () => {
    const schemaNames = Object.keys(spec.components?.schemas || {});
    const refs = [];
    JSON.stringify(spec, (key, val) => {
      if (key === '$ref' && typeof val === 'string') refs.push(val);
      return val;
    });

    const unresolved = refs.filter(ref => {
      const match = ref.match(/^#\/components\/schemas\/(.+)$/);
      return match && !schemaNames.includes(match[1]);
    });
    expect(unresolved).toEqual([]);
  });

  test('openapi.json in repo root is up to date', () => {
    let committed;
    try {
      committed = JSON.parse(readFileSync(join(__dirname, '..', '..', 'openapi.json'), 'utf8'));
    } catch {
      throw new Error('openapi.json not found -- run: npm run generate-openapi');
    }
    expect(committed).toEqual(spec);
  });
});
