/**
 * InternalAppMcpController L1 集成测试（协议合规防回归）
 *
 * 历史：旧版 controller 自创了 `{ok, data}` shape 不实现 `initialize`，Claude Code 客户端握手失败。
 * PoC 全用 curl 直调过没看真实客户端，导致整个 MCP 链路在真客户端上不可用。
 * 详见 .learnings/2026-05-18-mcp-controller-not-jsonrpc-compliant.md。
 *
 * 本测试用 supertest 直接 POST JSON-RPC 请求，验证 SDK 接管后的协议层契约：
 * - initialize 握手返合规 `{result: {protocolVersion, serverInfo, capabilities}}`
 * - tools/list 返 5 个工具 + 完整 inputSchema (JSON Schema)
 * - tools/call 返 MCP content + structuredContent 包装
 * - 鉴权失败返 JSON-RPC -32001 错误信封
 * - 终态守卫：DESTROYED app 重 deploy 返 app_in_terminal_state
 *
 * 真 Claude Code CLI 验证仍是必经（见 09-test §7.1），本测试做不到——它只是仿造
 * JSON-RPC body 验"服务端协议层不回退到旧的 {ok, data} 形态"。
 */

import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { createTestApp } from '../../helpers/app.helper';
import { InternalAppTokenService } from '@/modules/internal-app-platform/services/token.service';
import { PrismaService } from '@/core/database/prisma/prisma.service';

const T = 't-iap-mcp-ctl-';
const ORG_CODE = `${T}org`;
const MCP_PATH = '/api/v1/internal-apps/mcp';

/**
 * Streamable HTTP transport 用 SSE event-stream 返响应：
 *   event: message
 *   data: {"jsonrpc":"2.0","id":N,"result":{...}}
 * 提取首条 data 行的 JSON。鉴权失败走非 SDK 路径直接返 JSON，data: 不前缀。
 */
function parseMcpResponse(body: string): Record<string, unknown> {
  const dataLine = body.split('\n').find((l) => l.startsWith('data:'));
  if (dataLine) return JSON.parse(dataLine.slice('data:'.length).trim());
  return JSON.parse(body);
}

describe('InternalAppMcpController (L1, JSON-RPC 2.0 协议合规)', () => {
  let app: INestApplication;
  let tokenSvc: InternalAppTokenService;
  let prisma: PrismaService;
  let userId: string;
  let orgId: string;
  let employeeSlug: string;
  let tokenPlaintext: string;

  beforeAll(async () => {
    process.env.INTERNAL_APP_ENV_MASTER_KEY =
      process.env.INTERNAL_APP_ENV_MASTER_KEY ??
      'test_master_key_for_l1_integration_only_32+';
    app = await createTestApp();
    tokenSvc = app.get(InternalAppTokenService);
    prisma = app.get(PrismaService);
    await app.init();

    const org = await prisma.organization.upsert({
      where: { code: ORG_CODE },
      create: {
        code: ORG_CODE,
        name: `${T}org`,
        status: 'ACTIVE',
        isActive: true,
        settings: {},
        financialConfig: {},
        complianceConfig: {},
        order: 0,
        metadata: {},
      } as any,
      update: {},
    });
    orgId = org.id;

    const user = await prisma.user.create({
      data: {
        username: `${T}user`,
        email: `${T}user@test.local`,
        passwordHash: 'PLACEHOLDER',
        displayName: `${T}user`,
        status: 'ACTIVE',
        source: 'LOCAL',
        tenantId: 'default',
      },
    });
    userId = user.id;

    const issued = await tokenSvc.issue({
      userId,
      mailNickname: `${T}ctl`,
      organizationId: orgId,
    });
    employeeSlug = issued.employeeSlug;
    tokenPlaintext = issued.tokenPlaintext;
  });

  afterAll(async () => {
    await prisma.internalApp.deleteMany({ where: { employeeSlug } });
    await prisma.internalAppEmployeeToken.deleteMany({ where: { employeeSlug } });
    await prisma.employeeSlugBinding.deleteMany({ where: { employeeSlug } });
    await prisma.user.deleteMany({ where: { id: userId } });
    await prisma.organization.deleteMany({ where: { code: ORG_CODE } });
    await app.close();
  });

  it('未带 Bearer → 返 JSON-RPC -32001 错误信封（不是 {ok,false}）', async () => {
    const res = await request(app.getHttpServer())
      .post(MCP_PATH)
      .set('Accept', 'application/json, text/event-stream')
      .send({ jsonrpc: '2.0', id: 1, method: 'initialize' });

    expect(res.status).toBe(401);
    const body = parseMcpResponse(res.text);
    expect(body.jsonrpc).toBe('2.0');
    expect(body.error).toMatchObject({ code: -32001 });
    expect(body).not.toHaveProperty('ok'); // 防回归：不能再回到旧的 {ok:false} shape
  });

  it('initialize 握手返合规 protocolVersion + serverInfo + capabilities', async () => {
    const res = await request(app.getHttpServer())
      .post(MCP_PATH)
      .set('Authorization', `Bearer ${tokenPlaintext}`)
      .set('Accept', 'application/json, text/event-stream')
      .send({
        jsonrpc: '2.0',
        id: 1,
        method: 'initialize',
        params: {
          protocolVersion: '2024-11-05',
          capabilities: {},
          clientInfo: { name: 'integration-test', version: '1.0' },
        },
      });

    expect(res.status).toBe(200);
    const body = parseMcpResponse(res.text) as {
      jsonrpc: string;
      id: number;
      result: { protocolVersion: string; serverInfo: { name: string }; capabilities: { tools?: unknown } };
    };
    expect(body.jsonrpc).toBe('2.0');
    expect(body.id).toBe(1);
    expect(body.result.protocolVersion).toMatch(/^\d{4}-\d{2}-\d{2}$/);
    expect(body.result.serverInfo.name).toBe('ffoa-apps');
    expect(body.result.capabilities).toHaveProperty('tools');
  });

  it('tools/list 返 5 个工具 + 完整 JSON Schema inputSchema', async () => {
    const res = await request(app.getHttpServer())
      .post(MCP_PATH)
      .set('Authorization', `Bearer ${tokenPlaintext}`)
      .set('Accept', 'application/json, text/event-stream')
      .send({ jsonrpc: '2.0', id: 2, method: 'tools/list' });

    expect(res.status).toBe(200);
    const body = parseMcpResponse(res.text) as {
      result: { tools: Array<{ name: string; description: string; inputSchema: { type: string; properties: Record<string, unknown> } }> };
    };
    const names = body.result.tools.map((t) => t.name).sort();
    expect(names).toEqual(['deploy_prepare', 'destroy', 'env', 'list_apps', 'logs']);

    const deployPrepare = body.result.tools.find((t) => t.name === 'deploy_prepare')!;
    expect(deployPrepare.inputSchema.type).toBe('object');
    expect(deployPrepare.inputSchema.properties).toHaveProperty('app_slug');
  });

  it('tools/call list_apps 返 MCP content + structuredContent 双承载', async () => {
    const res = await request(app.getHttpServer())
      .post(MCP_PATH)
      .set('Authorization', `Bearer ${tokenPlaintext}`)
      .set('Accept', 'application/json, text/event-stream')
      .send({
        jsonrpc: '2.0',
        id: 3,
        method: 'tools/call',
        params: { name: 'list_apps', arguments: {} },
      });

    expect(res.status).toBe(200);
    const body = parseMcpResponse(res.text) as {
      result: { content: Array<{ type: string; text: string }>; structuredContent: { apps: unknown[] } };
    };
    expect(body.result.content[0].type).toBe('text');
    expect(JSON.parse(body.result.content[0].text)).toEqual(body.result.structuredContent);
    expect(body.result.structuredContent.apps).toEqual([]);
  });

  it('tools/call deploy_prepare 入参违反 zod schema → 返 -32602 标准错误码', async () => {
    const res = await request(app.getHttpServer())
      .post(MCP_PATH)
      .set('Authorization', `Bearer ${tokenPlaintext}`)
      .set('Accept', 'application/json, text/event-stream')
      .send({
        jsonrpc: '2.0',
        id: 4,
        method: 'tools/call',
        params: {
          name: 'deploy_prepare',
          arguments: { app_slug: 'xy' }, // too short, min 3
        },
      });

    expect(res.status).toBe(200);
    const body = parseMcpResponse(res.text) as {
      result: { content: Array<{ text: string }>; isError: boolean };
    };
    expect(body.result.isError).toBe(true);
    expect(body.result.content[0].text).toMatch(/-32602|too_small|Invalid arguments/i);
  });

  it('终态守卫：DESTROYED app 同 slug 重 deploy → app_in_terminal_state', async () => {
    // 预先种一个 DESTROYED 行（模拟员工先 destroy 再尝试同 slug 重 deploy）。
    // appSlug 上限 22 字符，T 前缀已 14 字符，所以剩 ≤8 字符。
    const slug = 'destroyed';
    const fullName = `FFAIApps/${employeeSlug}-${slug}`;
    await prisma.internalApp.create({
      data: {
        employeeSlug,
        appSlug: slug,
        runtime: 'node',
        status: 'DESTROYED',
        url: `https://${employeeSlug}-${slug}.apps.test`,
        giteaRepoFullName: fullName,
        organizationId: orgId,
        createdById: userId,
        destroyedAt: new Date(),
        retentionUntil: new Date(Date.now() + 30 * 24 * 3600 * 1000),
      },
    });

    const res = await request(app.getHttpServer())
      .post(MCP_PATH)
      .set('Authorization', `Bearer ${tokenPlaintext}`)
      .set('Accept', 'application/json, text/event-stream')
      .send({
        jsonrpc: '2.0',
        id: 5,
        method: 'tools/call',
        params: {
          name: 'deploy_prepare',
          arguments: {
            app_slug: slug,
            detected: {
              has_package_json: true,
              has_start_script: true,
              has_index_html: false,
            },
          },
        },
      });

    expect(res.status).toBe(200);
    const body = parseMcpResponse(res.text) as {
      result: { content: Array<{ text: string }>; isError: boolean; structuredContent: { error: { code: string } } };
    };
    expect(body.result.isError).toBe(true);
    expect(body.result.structuredContent.error.code).toBe('app_in_terminal_state');
    expect(body.result.content[0].text).toContain('app_in_terminal_state');
  });
});
