/**
 * @file test-fixture.ts
 * @description Jest-based fixture system for MCP testing
 *
 * Provides a Playwright-like fixture API for testing FrontMCP servers:
 *
 * @example
 * ```typescript
 * import { test, expect } from '@frontmcp/testing';
 *
 * test.use({
 *   server: './src/main.ts',
 *   port: 3003,
 * });
 *
 * test('server exposes tools', async ({ mcp }) => {
 *   const tools = await mcp.tools.list();
 *   expect(tools).toContainTool('my-tool');
 * });
 * ```
 */

import {
  afterAll as _afterAll,
  afterEach as _afterEach,
  beforeAll as _beforeAll,
  beforeEach as _beforeEach,
  describe as _describe,
  it as _it,
} from '@jest/globals';

import { TestTokenFactory } from '../auth/token-factory';
import { McpTestClient } from '../client/mcp-test-client';
import { McpTestClientBuilder } from '../client/mcp-test-client.builder';
import { TestServer } from '../server/test-server';
import type {
  AuthFixture,
  ServerFixture,
  TestConfig,
  TestFixtures,
  TestFn,
  TestUser,
  TestWithFixtures,
} from './fixture-types';

// Re-export with compatible types (Jest globals may differ in type signatures
// between @jest/globals and the global declarations used by our TestWithFixtures)
const describe = _describe as unknown as jest.Describe;
const beforeAll = _beforeAll as unknown as jest.Lifecycle;
const beforeEach = _beforeEach as unknown as jest.Lifecycle;
const afterEach = _afterEach as unknown as jest.Lifecycle;
const afterAll = _afterAll as unknown as jest.Lifecycle;
const it = _it as unknown as jest.It;

// ═══════════════════════════════════════════════════════════════════
// GLOBAL STATE
// ═══════════════════════════════════════════════════════════════════

/** Current test configuration (set via test.use()) */
let currentConfig: TestConfig = {};

/** Server instance (shared across tests in a file) */
let serverInstance: TestServer | null = null;

/** Token factory instance (shared across tests in a file) */
let tokenFactory: TestTokenFactory | null = null;

/** Track if server was started by us (vs external) */
let serverStartedByUs = false;

// ═══════════════════════════════════════════════════════════════════
// FIXTURE SETUP/TEARDOWN
// ═══════════════════════════════════════════════════════════════════

/**
 * Initialize shared resources (server, token factory) once per test file
 */
async function initializeSharedResources(): Promise<void> {
  // Create token factory if not exists.
  //
  // When the test config passes a JWT_SECRET to the server (gateway modes —
  // auth.mode public/local/orchestrated verify tokens against their own HS256
  // secret), sign fixture tokens with that SAME secret so `auth.createToken`
  // mints genuinely-valid tokens (the server now cryptographically verifies
  // the signature). Without a shared secret the factory stays RS256 + JWKS for
  // transparent mode.
  if (!tokenFactory) {
    const sharedSecret = currentConfig.env?.['JWT_SECRET'];
    tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret } : {});
  }

  // Start or connect to server if not exists
  if (!serverInstance) {
    if (currentConfig.baseUrl) {
      // Connect to existing external server
      serverInstance = TestServer.connect(currentConfig.baseUrl);
      serverStartedByUs = false;
    } else if (currentConfig.server) {
      // Start new server with detailed error handling
      const serverCommand = resolveServerCommand(currentConfig.server);
      const isDebug =
        currentConfig.logLevel === 'debug' || process.env['DEBUG'] === '1' || process.env['DEBUG_SERVER'] === '1';

      if (isDebug) {
        console.log(`[TestFixture] Starting server: ${serverCommand}`);
      }

      try {
        serverInstance = await TestServer.start({
          project: currentConfig.project,
          port: currentConfig.port,
          command: serverCommand,
          env: currentConfig.env,
          startupTimeout: currentConfig.startupTimeout ?? 30000,
          debug: isDebug,
        });
        serverStartedByUs = true;

        if (isDebug) {
          console.log(`[TestFixture] Server started at ${serverInstance.info.baseUrl}`);
        }
      } catch (error) {
        // Re-throw with additional context
        const errMsg = error instanceof Error ? error.message : String(error);
        throw new Error(
          `Failed to start test server.\n\n` +
            `Server entry: ${currentConfig.server}\n` +
            `Project: ${currentConfig.project ?? 'default'}\n` +
            `Command: ${serverCommand}\n\n` +
            `Error: ${errMsg}`,
        );
      }
    } else {
      throw new Error(
        'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option',
      );
    }
  }
}

/**
 * Create fixtures for a single test
 */
async function createTestFixtures(): Promise<TestFixtures> {
  // Ensure shared resources are initialized
  await initializeSharedResources();

  // Ensure shared resources are available
  if (!serverInstance) {
    throw new Error('Server instance not initialized');
  }
  if (!tokenFactory) {
    throw new Error('Token factory not initialized');
  }

  // Create MCP client for this test
  // Pass publicMode if configured to skip authentication
  const clientInstance = await McpTestClient.create({
    baseUrl: serverInstance.info.baseUrl,
    transport: currentConfig.transport ?? 'streamable-http',
    publicMode: currentConfig.publicMode,
  }).buildAndConnect();

  // Build fixtures
  const auth = createAuthFixture(tokenFactory);
  const server = createServerFixture(serverInstance);

  return {
    mcp: clientInstance,
    auth,
    server,
  };
}

/**
 * Clean up fixtures after a single test
 * @param fixtures - The test fixtures to clean up
 * @param testFailed - Whether the test failed (to output server logs)
 */
async function cleanupTestFixtures(fixtures: TestFixtures, testFailed = false): Promise<void> {
  // Output server logs if test failed (helps with debugging)
  if (testFailed && serverInstance) {
    const logs = serverInstance.getLogs();
    if (logs.length > 0) {
      console.error('\n[TestFixture] === Server Logs (test failed) ===');
      // Show last 50 lines of logs to avoid flooding output
      const recentLogs = logs.slice(-50);
      if (logs.length > 50) {
        console.error(`[TestFixture] (showing last 50 of ${logs.length} log entries)`);
      }
      console.error(recentLogs.join('\n'));
      console.error('[TestFixture] === End Server Logs ===\n');
    }
  }

  // Disconnect client
  if (fixtures.mcp.isConnected()) {
    await fixtures.mcp.disconnect();
  }
}

/**
 * Clean up shared resources after all tests in a file
 */
async function cleanupSharedResources(): Promise<void> {
  // Only stop server if we started it
  if (serverInstance && serverStartedByUs) {
    await serverInstance.stop();
  }
  serverInstance = null;
  tokenFactory = null;
  serverStartedByUs = false;
}

// ═══════════════════════════════════════════════════════════════════
// FIXTURE FACTORIES
// ═══════════════════════════════════════════════════════════════════

/**
 * Create the auth fixture from token factory
 */
function createAuthFixture(factory: TestTokenFactory): AuthFixture {
  const users: Record<string, TestUser> = {
    admin: {
      sub: 'admin-001',
      scopes: ['admin:*', 'read', 'write', 'delete'],
      email: 'admin@test.local',
      name: 'Test Admin',
    },
    user: {
      sub: 'user-001',
      scopes: ['read', 'write'],
      email: 'user@test.local',
      name: 'Test User',
    },
    readOnly: {
      sub: 'readonly-001',
      scopes: ['read'],
      email: 'readonly@test.local',
      name: 'Read Only User',
    },
  };

  return {
    createToken: (opts) =>
      factory.createTestToken({
        sub: opts.sub,
        scopes: opts.scopes,
        claims: {
          email: opts.email,
          name: opts.name,
          ...opts.claims,
        },
        exp: opts.expiresIn,
      }),

    createExpiredToken: (opts) => factory.createExpiredToken(opts),

    createInvalidToken: (opts) => factory.createTokenWithInvalidSignature(opts),

    users: {
      admin: users['admin'],
      user: users['user'],
      readOnly: users['readOnly'],
    },

    getJwks: () => factory.getPublicJwks(),

    getIssuer: () => factory.getIssuer(),

    getAudience: () => factory.getAudience(),
  };
}

/**
 * Create the server fixture from test server
 */
function createServerFixture(server: TestServer): ServerFixture {
  return {
    info: server.info,

    createClient: async (opts) => {
      // Inherit publicMode from current config when creating additional clients
      // This ensures all clients connect to a public server correctly
      return McpTestClient.create({
        baseUrl: server.info.baseUrl,
        transport: opts?.transport ?? 'streamable-http',
        auth: opts?.token ? { token: opts.token } : undefined,
        clientInfo: opts?.clientInfo,
        publicMode: currentConfig.publicMode,
      }).buildAndConnect();
    },

    createClientBuilder: () => {
      // Return a pre-configured builder with the server's base URL and publicMode
      // This allows full customization including platform-specific capabilities
      const builder = new McpTestClientBuilder({
        baseUrl: server.info.baseUrl,
        publicMode: currentConfig.publicMode,
      });
      return builder;
    },

    restart: () => server.restart(),

    getLogs: () => server.getLogs(),

    clearLogs: () => server.clearLogs(),
  };
}

/**
 * Resolve server entry to a command
 */
function resolveServerCommand(server: string): string {
  // If it's already a command (contains spaces), use as-is
  if (server.includes(' ')) {
    return server;
  }
  // Otherwise, run with tsx
  return `npx tsx ${server}`;
}

// ═══════════════════════════════════════════════════════════════════
// TEST FUNCTION WITH FIXTURES
// ═══════════════════════════════════════════════════════════════════

/**
 * Enhanced test function that provides fixtures
 */
function testWithFixtures(name: string, fn: TestFn): void {
  it(name, async () => {
    const fixtures = await createTestFixtures();
    let testFailed = false;
    try {
      await fn(fixtures);
    } catch (error) {
      testFailed = true;
      throw error;
    } finally {
      await cleanupTestFixtures(fixtures, testFailed);
    }
  });
}

/**
 * Configure test fixtures for the current test file/suite
 */
function use(config: TestConfig): void {
  // Merge with existing config
  currentConfig = { ...currentConfig, ...config };

  // Register cleanup hook if not already done
  // This ensures server is stopped after all tests in the file
  afterAll(async () => {
    await cleanupSharedResources();
  });
}

/**
 * Skip a test
 */
function skip(name: string, fn: TestFn): void {
  it.skip(name, async () => {
    const fixtures = await createTestFixtures();
    let testFailed = false;
    try {
      await fn(fixtures);
    } catch (error) {
      testFailed = true;
      throw error;
    } finally {
      await cleanupTestFixtures(fixtures, testFailed);
    }
  });
}

/**
 * Run only this test
 */
function only(name: string, fn: TestFn): void {
  it.only(name, async () => {
    const fixtures = await createTestFixtures();
    let testFailed = false;
    try {
      await fn(fixtures);
    } catch (error) {
      testFailed = true;
      throw error;
    } finally {
      await cleanupTestFixtures(fixtures, testFailed);
    }
  });
}

/**
 * Mark test as todo
 */
function todo(name: string): void {
  it.todo(name);
}

// ═══════════════════════════════════════════════════════════════════
// ATTACH STATIC METHODS
// ═══════════════════════════════════════════════════════════════════

// Cast to the full interface type
const test = testWithFixtures as TestWithFixtures;

// Attach configuration method
test.use = use;

// Attach Jest lifecycle methods
test.describe = describe;
test.beforeAll = beforeAll;
test.beforeEach = beforeEach;
test.afterEach = afterEach;
test.afterAll = afterAll;

// Attach test modifiers
test.skip = skip;
test.only = only;
test.todo = todo;

// ═══════════════════════════════════════════════════════════════════
// EXPORTS
// ═══════════════════════════════════════════════════════════════════

export { test };

// Also export for advanced use cases
export { createTestFixtures, cleanupTestFixtures, initializeSharedResources, cleanupSharedResources };
