// @vitest-environment node
/**
 * request-handler.ts のユニットテスト。
 *
 * 対象:
 *   1. makeExtendedCardProvider — principal あり → extended card / なし → base card
 *   2. mountA2aRequestHandler supertest smoke — Bearer 無し POST /a2a が 500 にならない
 *
 * Task 8 の本番 token round-trip テストは別ファイルに置く（e2e 扱い）。
 */
import { describe, it, expect, beforeEach } from 'vitest';
import express from 'express';
import request from 'supertest';
import { Repository } from '../../db/repository.js';
import { buildBaseCard } from './agent-card.js';
import { makeExtendedCardProvider, mountA2aRequestHandler } from './request-handler.js';
import type { A2aPrincipal } from './token-auth.js';

// ─── seed helpers ─────────────────────────────────────────────────────────────

function seedRepo(repo: Repository) {
  (repo as any).db.prepare(
    "INSERT INTO users (id, email, name, avatar_url, role, status, created_at, updated_at) " +
    "VALUES ('u1','u1@test','User One',NULL,'user','active','2026-01-01','2026-01-01')",
  ).run();
  (repo as any).db.prepare(
    "INSERT INTO spaces (id, title, kind, owner_id, visibility) VALUES ('s1','S1','case','u1','private')",
  ).run();
  repo.setSpaceA2aSkills('s1', ['research']);
  repo.createA2aDelegation({
    id: 'd1', userId: 'u1', clientId: 'cli1', grantId: 'g1',
    grantedSpaceIds: ['s1'], grantedSkills: ['research'],
    audience: 'https://m/a2a', expiresAt: null, revokedAt: null,
  });
}

/** Full A2aPrincipal that matches the seeded delegation. */
const FAKE_PRINCIPAL: A2aPrincipal = {
  actingUserId: 'u1',
  clientId: 'cli1',
  grantId: 'g1',
  delegation: {
    userId: 'u1', clientId: 'cli1', grantId: 'g1',
    grantedSpaceIds: ['s1'], grantedSkills: ['research'],
    expiresAt: null, revokedAt: null,
  },
};

/** Minimal fake OIDC provider (no tokens → always unauthenticated). */
function fakeProvider() {
  return { AccessToken: { async find(_v: string) { return undefined; } } } as any;
}

const BASE_DEPS = { baseUrl: 'https://m', issuer: 'https://m/oidc', version: '1.0.0' };

// ─── makeExtendedCardProvider ─────────────────────────────────────────────────

describe('makeExtendedCardProvider', () => {
  let repo: Repository;

  beforeEach(() => {
    repo = new Repository(':memory:');
    seedRepo(repo);
  });

  it('context=undefined (no call context) → base card with empty skills', async () => {
    const baseCard = buildBaseCard(BASE_DEPS);
    const provider = makeExtendedCardProvider(repo, baseCard, BASE_DEPS);

    const card = await provider(undefined);
    expect(card.skills).toEqual([]);
  });

  it('context.user has no a2aPrincipal (UnauthenticatedUser) → base card', async () => {
    const baseCard = buildBaseCard(BASE_DEPS);
    const provider = makeExtendedCardProvider(repo, baseCard, BASE_DEPS);

    const ctx = { user: { isAuthenticated: false, userName: '' } } as any;
    const card = await provider(ctx);
    expect(card.skills).toEqual([]);
  });

  it('authenticated principal with live delegation + space allowlist → extended card with skills', async () => {
    const baseCard = buildBaseCard(BASE_DEPS);
    const provider = makeExtendedCardProvider(repo, baseCard, BASE_DEPS);

    const ctx = {
      user: { a2aPrincipal: FAKE_PRINCIPAL, isAuthenticated: true, userName: 'u1' },
    } as any;
    const card = await provider(ctx);
    // space s1 has allowlist ['research'], delegation grants ['research'] → intersection ['research']
    expect(card.skills.map((s: { id: string }) => s.id)).toEqual(['research']);
  });

  it('principal whose acting user does not exist → falls back to base card', async () => {
    const baseCard = buildBaseCard(BASE_DEPS);
    const provider = makeExtendedCardProvider(repo, baseCard, BASE_DEPS);

    const ghost: A2aPrincipal = { ...FAKE_PRINCIPAL, actingUserId: 'ghost' };
    const ctx = { user: { a2aPrincipal: ghost, isAuthenticated: true, userName: 'ghost' } } as any;
    const card = await provider(ctx);
    expect(card.skills).toEqual([]);
  });

  it('principal whose granted skill is not in any space allowlist → empty skills on extended card', async () => {
    const baseCard = buildBaseCard(BASE_DEPS);
    const provider = makeExtendedCardProvider(repo, baseCard, BASE_DEPS);

    const restrictedPrincipal: A2aPrincipal = {
      ...FAKE_PRINCIPAL,
      delegation: { ...FAKE_PRINCIPAL.delegation, grantedSkills: ['admin'] }, // 'admin' not in s1 allowlist
    };
    const ctx = { user: { a2aPrincipal: restrictedPrincipal, isAuthenticated: true, userName: 'u1' } } as any;
    const card = await provider(ctx);
    expect(card.skills).toEqual([]);
  });
});

// ─── supertest smoke ──────────────────────────────────────────────────────────

describe('mountA2aRequestHandler – smoke', () => {
  it('POST /a2a without Bearer token does not crash (no 500)', async () => {
    const repo = new Repository(':memory:');
    seedRepo(repo);

    const app = express();
    mountA2aRequestHandler(app, { provider: fakeProvider(), repo, ...BASE_DEPS });

    const res = await request(app)
      .post('/a2a')
      .set('Content-Type', 'application/json')
      .send({
        jsonrpc: '2.0',
        id: 1,
        method: 'message/send',
        params: {
          message: {
            messageId: 'm1',
            role: 'user',
            parts: [{ kind: 'text', text: 'hello' }],
          },
        },
      });

    // Handler must not 500-crash and must return a valid JSON-RPC 2.0 envelope.
    // message/send is NOT gated by AuthGatedRequestHandler — the executor creates
    // the task (returning a result), then fails it asynchronously. So the HTTP
    // response carries either a JSON-RPC result or error, never a 500.
    expect(res.status).not.toBe(500);
    expect(res.headers['content-type']).toMatch(/json/);
    expect(res.body.jsonrpc).toBe('2.0');
    expect('result' in res.body || 'error' in res.body).toBe(true);
  });
});

// ─── body size limit ──────────────────────────────────────────────────────────

describe('mountA2aRequestHandler – body size limit', () => {
  it('POST /a2a with body exceeding maxPayloadBytes returns HTTP 413', async () => {
    const repo = new Repository(':memory:');
    seedRepo(repo);

    const app = express();
    // Mount with a tiny limit (50 bytes) so a normal JSON-RPC body exceeds it.
    mountA2aRequestHandler(app, {
      provider: fakeProvider(),
      repo,
      ...BASE_DEPS,
      limits: { maxPayloadBytes: 50 },
    });

    // This body is well over 50 bytes.
    const bigBody = JSON.stringify({
      jsonrpc: '2.0',
      id: 1,
      method: 'message/send',
      params: {
        message: {
          messageId: 'm1',
          role: 'user',
          parts: [{ kind: 'text', text: 'hello world — this payload deliberately exceeds the tiny limit' }],
        },
      },
    });

    const res = await request(app)
      .post('/a2a')
      .set('Content-Type', 'application/json')
      .send(bigBody);

    // express.json({ limit }) calls next(err) with a PayloadTooLargeError (status 413)
    // before the SDK handler sees the body. Express's default error handler surfaces it.
    expect(res.status).toBe(413);
  });
});
