import { afterEach, describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as http from 'node:http';
import * as os from 'node:os';
import * as path from 'node:path';
import { runMxCli, parseLastJson, assertExitCode } from '../helpers/cli-harness.js';
import { createMatrixPackage } from '../helpers/package-fixtures.js';

interface ISearchFixture {
  readonly registryUrl: string;
  readonly searchRequests: () => number;
  readonly authorizationHeaders: () => readonly string[];
  readonly searchQueries: () => readonly string[];
  close(): Promise<void>;
}

function addressPort(server: http.Server): number {
  const address = server.address();
  if (!address || typeof address === 'string') {
    throw new Error('Search registry fixture is not listening on a TCP port');
  }
  return address.port;
}

async function startSearchRegistryFixture(): Promise<ISearchFixture> {
  let searchRequestCount = 0;
  const authorizationHeaders: string[] = [];
  const searchQueries: string[] = [];

  const server = http.createServer((request, response) => {
    const registryUrl = `http://127.0.0.1:${addressPort(server)}/`;
    const requestUrl = new URL(request.url ?? '/', registryUrl);
    const auth = request.headers.authorization;
    if (typeof auth === 'string') {
      authorizationHeaders.push(auth);
    }

    if (requestUrl.pathname === '/-/v1/search') {
      searchRequestCount += 1;
      searchQueries.push(requestUrl.searchParams.get('text') ?? '');
      response.writeHead(200, { 'content-type': 'application/json' });
      response.end(JSON.stringify({
        objects: [
          {
            package: {
              name: '@open-matrix/chat',
              version: '0.2.3',
              description: 'Chat package',
              keywords: ['matrix', 'chat'],
            },
          },
          {
            package: {
              scope: '@open-matrix',
              name: 'flowpad',
              version: '0.3.0',
              description: 'FlowPad package',
              keywords: ['matrix', 'flowpad'],
            },
          },
          {
            package: {
              name: '@open-matrix/director',
              version: '0.1.0',
              description: 'Director package',
              keywords: ['matrix', 'director'],
            },
          },
        ],
      }));
      return;
    }

    response.writeHead(404, { 'content-type': 'application/json' });
    response.end(JSON.stringify({ error: `Not found: ${requestUrl.pathname}` }));
  });

  await new Promise<void>((resolve, reject) => {
    server.once('error', reject);
    server.listen(0, '127.0.0.1', () => {
      server.off('error', reject);
      resolve();
    });
  });

  return {
    registryUrl: `http://127.0.0.1:${addressPort(server)}/`,
    searchRequests: () => searchRequestCount,
    authorizationHeaders: () => [...authorizationHeaders],
    searchQueries: () => [...searchQueries],
    async close(): Promise<void> {
      await new Promise<void>((resolve, reject) => {
        server.close((error) => (error ? reject(error) : resolve()));
      });
    },
  };
}

describe('FP-30 mx search command', () => {
  const tempDirs: string[] = [];
  const closeFns: Array<() => Promise<void>> = [];
  const restoreEnvFns: Array<() => void> = [];

  afterEach(async () => {
    while (restoreEnvFns.length > 0) {
      const restore = restoreEnvFns.pop();
      if (restore) {
        restore();
      }
    }
    while (closeFns.length > 0) {
      const close = closeFns.pop();
      if (close) {
        await close();
      }
    }
    while (tempDirs.length > 0) {
      const dir = tempDirs.pop();
      if (dir) {
        fs.rmSync(dir, { recursive: true, force: true });
      }
    }
  });

  function useMatrixHome(matrixHome: string): void {
    const previousMatrixHome = process.env.MATRIX_HOME;
    const previousCliConfig = process.env.MATRIX_CLI_CONFIG;
    const previousPackageRegistry = process.env.MATRIX_PACKAGE_REGISTRY;
    const previousRegistryUrl = process.env.MATRIX_REGISTRY_URL;
    process.env.MATRIX_HOME = matrixHome;
    delete process.env.MATRIX_CLI_CONFIG;
    delete process.env.MATRIX_PACKAGE_REGISTRY;
    delete process.env.MATRIX_REGISTRY_URL;
    restoreEnvFns.push(() => {
      restoreEnv('MATRIX_HOME', previousMatrixHome);
      restoreEnv('MATRIX_CLI_CONFIG', previousCliConfig);
      restoreEnv('MATRIX_PACKAGE_REGISTRY', previousPackageRegistry);
      restoreEnv('MATRIX_REGISTRY_URL', previousRegistryUrl);
    });
  }

  function restoreEnv(key: string, value: string | undefined): void {
    if (value === undefined) {
      delete process.env[key];
      return;
    }
    process.env[key] = value;
  }

  it('searches the configured npm registry with stored registry credentials', async () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-search-'));
    tempDirs.push(root);
    useMatrixHome(path.join(root, '.matrix'));
    const workspaceDir = path.join(root, 'workspace');
    fs.mkdirSync(workspaceDir, { recursive: true });
    const credentialsFile = path.join(root, '.matrix', 'credentials.json');
    const registry = await startSearchRegistryFixture();
    closeFns.push(registry.close);

    const setConfig = await runMxCli([
      'node',
      'mx',
      'config',
      'set',
      'registry',
      registry.registryUrl,
      '--json',
    ], { cwd: workspaceDir });
    assertExitCode(setConfig, 0);

    const loginResult = await runMxCli([
      'node',
      'mx',
      'login',
      '--username',
      'alice',
      '--token',
      'token-search',
      '--credentials-file',
      credentialsFile,
      '--json',
    ], { cwd: workspaceDir });
    assertExitCode(loginResult, 0);

    const searchResult = await runMxCli([
      'node',
      'mx',
      'search',
      'chat',
      '--credentials-file',
      credentialsFile,
      '--limit',
      '5',
      '--json',
    ], { cwd: workspaceDir });
    assertExitCode(searchResult, 0);
    const payload = parseLastJson(searchResult.logs) as {
      query: string;
      registry: string;
      registrySource: string;
      results: Array<{ packageName: string; version: string; description?: string; keywords: string[] }>;
    };
    assert.equal(payload.query, 'chat');
    assert.equal(payload.registry, registry.registryUrl);
    assert.equal(payload.registrySource, 'config');
    assert.equal(payload.results.length, 3);
    assert.equal(payload.results[0]?.packageName, '@open-matrix/chat');
    assert.equal(payload.results[0]?.version, '0.2.3');
    assert.deepEqual(payload.results[0]?.keywords, ['matrix', 'chat']);
    assert.equal(payload.results[1]?.packageName, '@open-matrix/flowpad');
    assert.equal(payload.results[1]?.version, '0.3.0');
    assert.equal(registry.searchRequests(), 1);
    assert.deepEqual(registry.searchQueries(), ['chat']);
    assert.equal(
      registry.authorizationHeaders().some((header) => header.includes('token-search')),
      true,
    );
  });

  it('searches the local package discovery index created by publish --registry-dir', async () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-discovery-search-'));
    tempDirs.push(root);
    useMatrixHome(path.join(root, '.matrix'));
    const workspaceDir = path.join(root, 'workspace');
    fs.mkdirSync(workspaceDir, { recursive: true });
    const registryDir = path.join(root, '.matrix', 'registry');
    const credentialsFile = path.join(root, '.matrix', 'credentials.json');
    const fixture = createMatrixPackage(path.join(root, 'source'), {
      packageName: '@acme/semantic-chat',
      version: '1.2.3',
      componentType: 'SemanticChatActor',
      mount: 'semantic.chat',
    });

    const manifestPath = path.join(fixture.packageDir, 'matrix.json');
    const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as Record<string, unknown>;
    manifest.description = 'Semantic package for chat workflows';
    manifest.class = 'hybrid';
    manifest.tags = ['communication'];
    manifest.capabilities = ['conversation'];
    manifest.webapp = {
      distDir: 'dist',
      entry: 'index.html',
      appName: 'chat',
      displayName: 'Semantic Chat',
      icon: '💬',
      navOrder: 30,
      description: 'Conversation workspace',
      shells: ['platform', 'edge'],
    };
    fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
    fs.mkdirSync(path.join(fixture.packageDir, 'dist'), { recursive: true });
    fs.writeFileSync(path.join(fixture.packageDir, 'dist', 'index.html'), '<!doctype html><title>Semantic Chat</title>', 'utf8');
    fs.writeFileSync(
      path.join(fixture.packageDir, 'src', 'index.ts'),
      `export class SemanticChatActor {
  static description = 'Coordinates indexed chat conversations';
  static kind = 'service';
  static accepts = {
    'chat.status': { description: 'Read chat status' },
    'chat.send': { description: 'Send a chat message' }
  };
  static emits = {
    'chat.message.created': { description: 'Message created' }
  };
  static skills = {
    'conversation-management': { description: 'Manage conversation state' }
  };
  static tags = ['chat', 'semantic'];
  static capabilities = ['messaging'];
}
`,
      'utf8',
    );

    const loginResult = await runMxCli([
      'node',
      'mx',
      'login',
      '--username',
      'alice',
      '--token',
      'token-discovery-search',
      '--credentials-file',
      credentialsFile,
      '--json',
    ], { cwd: workspaceDir });
    assertExitCode(loginResult, 0);

    const publishResult = await runMxCli([
      'node',
      'mx',
      'publish',
      '--registry-dir',
      registryDir,
      '--credentials-file',
      credentialsFile,
      '--json',
    ], { cwd: fixture.packageDir });
    assertExitCode(publishResult, 0);
    assert.equal(fs.existsSync(path.join(registryDir, 'discovery-index.json')), true);

    const searchResult = await runMxCli([
      'node',
      'mx',
      'search',
      'conversation',
      '--registry-dir',
      registryDir,
      '--limit',
      '5',
      '--json',
    ], { cwd: workspaceDir });
    assertExitCode(searchResult, 0);
    const payload = parseLastJson(searchResult.logs) as {
      query: string;
      registry: string;
      registrySource: string;
      results: Array<{
        packageName: string;
        version: string;
        displayName?: string;
        inferredKind?: string;
        keywords: readonly string[];
        matchedFields?: readonly string[];
      }>;
    };
    assert.equal(payload.query, 'conversation');
    assert.equal(payload.registry, registryDir);
    assert.equal(payload.registrySource, 'local');
    assert.equal(payload.results.length, 1);
    assert.equal(payload.results[0]?.packageName, '@acme/semantic-chat');
    assert.equal(payload.results[0]?.version, '1.2.3');
    assert.equal(payload.results[0]?.displayName, 'Semantic Chat');
    assert.equal(payload.results[0]?.inferredKind, 'app');
    assert.equal(payload.results[0]?.keywords.includes('conversation-management'), true);
    assert.equal(payload.results[0]?.keywords.includes('edge'), true);
    assert.equal(payload.results[0]?.matchedFields?.includes('component.skills'), true);
  });
});
