// @vitest-environment node
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import express from 'express';
import http from 'http';
import type Provider from 'oidc-provider';
import { createHash, randomBytes, randomUUID } from 'crypto';
import { mkdtempSync, mkdirSync, writeFileSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { DefaultRequestHandler } from '@a2a-js/sdk/server';
import { jsonRpcHandler } from '@a2a-js/sdk/server/express';
import { Repository, localTaskRepoName } from '../../db/repository.js';
import { createA2aOidcProvider, mountA2aOidc } from './oidc-provider.js';
import { buildBaseCard } from './agent-card.js';
import { makeExtendedCardProvider } from './request-handler.js';
import { SqliteA2aTaskStore } from './task-store.js';
import { MaestroA2aExecutor } from './executor.js';
import { makeA2aUserBuilder } from './user-builder.js';
import { A2aTaskReconciler } from './reconciler.js';
import { b64url, Client } from './__e2e-helpers.js';

/**
 * Plan 2C-1 Task 6 — 非ブロッキング A2A の e2e ゲート。
 *
 * 実 oidc-provider（Plan 1）+ consent（Plan 2A）+ DefaultRequestHandler（Plan 2B）+
 * A2aTaskReconciler（Plan 2C-1）を同一 http server に相乗りさせ、本物の
 * authorization_code + PKCE トークンで JSON-RPC を叩く。実 Worker / 実 LLM は使わず、
 * DB のジョブを succeeded に反転させて完了をシミュレートする（a2a-exec-e2e と同技法）。
 *
 * request-handler.ts の mountA2aRequestHandler は executor の poll 挙動を注入できないため、
 * ここでは同じ部品（buildBaseCard / makeExtendedCardProvider / SqliteA2aTaskStore /
 * MaestroA2aExecutor / makeA2aUserBuilder）で handler を組み、executor の sleep を
 * 制御可能にする。`executorParked = true` の間、executor は初期 Task（submitted）を
 * publish した直後に永久に park する（＝クライアント切断/executor 死亡の等価物）。
 * これにより「reconciler だけが収束させる」ことを曖昧さなく実証できる。
 * case 4（blocking:true 回帰）だけ park を解除し、通常の poll → completed を通す。
 *
 * 検証:
 *   1. blocking:false → 即座に非 terminal Task が返る（ジョブ完了を待たない）
 *   2. job succeeded + output → reconcileOnce() → tasks/get が completed + artifact
 *   3. 再起動シナリオ: executor を通さず working task を仕込む → reconcileOnce() だけで収束
 *   4. blocking:true（既定）は従来どおり完了まで待って completed を返す（回帰）
 */
describe('A2A async e2e (blocking:false + tasks/get + reconciler convergence + restart)', () => {
  let server: http.Server;
  let base: string;
  let audience: string;
  let repo: Repository;
  let provider: Provider;
  let reconciler: A2aTaskReconciler;

  const clientId = 'a2a_async_e2e';
  const redirectUri = 'https://client.test/cb';
  const userId = 'user-1';
  const spaceId = 'space-research';

  const RESULT_TEXT = 'A2A async round-trip artifact OK — 非ブロッキングの結果ファイル';
  let outDir: string; // tmp workspace with output/result.txt

  /** executor の poll を park（永久 sleep）にするか通常 poll にするかを切り替える可変フラグ。 */
  let executorParked = true;

  /** ジョブを succeeded に反転し、worktree_path を outDir に向ける（成果物解決用）。 */
  function completeJob(jobId: string): void {
    (repo as any).db
      .prepare("UPDATE jobs SET status = 'succeeded', worktree_path = ? WHERE id = ?")
      .run(outDir, jobId);
  }

  /** case 4 用の一時 completer: queued ジョブを succeeded へ反転し続ける（exec-e2e と同技法）。 */
  function startCompleter(): () => void {
    let stop = false;
    const db = (repo as any).db;
    const selQueued = db.prepare("SELECT id, issue_number FROM jobs WHERE status = 'queued'");
    const updJob = db.prepare("UPDATE jobs SET status = 'succeeded', worktree_path = ? WHERE id = ?");
    const updTask = db.prepare("UPDATE local_tasks SET workspace_path = ? WHERE id = ?");
    (async () => {
      while (!stop) {
        const rows = selQueued.all() as Array<{ id: string; issue_number: number }>;
        for (const r of rows) {
          updJob.run(outDir, r.id);
          updTask.run(outDir, r.issue_number);
        }
        await new Promise(res => setTimeout(res, 5));
      }
    })();
    return () => { stop = true; };
  }

  beforeAll(async () => {
    repo = new Repository(':memory:');

    repo.createA2aClient({
      clientId, name: 'Async E2E', redirectUris: [redirectUri],
      secretHash: null, tokenEndpointAuthMethod: 'none',
      agentCardUrl: null, issuer: null, keyFingerprint: null, status: 'active', createdBy: 'admin',
    });

    (repo as any).db.prepare(
      "INSERT INTO users (id, email, name, avatar_url, role, status, created_at, updated_at) " +
      "VALUES (?, ?, ?, NULL, 'user', 'active', '2026-01-01', '2026-01-01')",
    ).run(userId, 'u1@test', 'User One');

    (repo as any).db.prepare(
      "INSERT INTO spaces (id, title, kind, owner_id, visibility) VALUES (?, 'Research', 'case', ?, 'private')",
    ).run(spaceId, userId);
    repo.setSpaceA2aSkills(spaceId, ['research']);

    outDir = mkdtempSync(join(tmpdir(), 'a2a-async-ws-'));
    mkdirSync(join(outDir, 'output'), { recursive: true });
    writeFileSync(join(outDir, 'output', 'result.txt'), RESULT_TEXT);

    const secretsDir = mkdtempSync(join(tmpdir(), 'a2a-async-e2e-'));
    const app = express();
    app.use((req, _res, next) => { (req as any).user = { id: userId, role: 'user' }; next(); });

    server = http.createServer(app);
    await new Promise<void>(r => server.listen(0, '127.0.0.1', r));
    const port = (server.address() as any).port;
    base = `http://127.0.0.1:${port}`;
    audience = `${base}/a2a`;

    provider = createA2aOidcProvider({
      repo, secretsDir,
      issuer: `${base}/oidc`,
      resourceAudience: audience,
      cookieKeys: ['test-cookie-key'],
    });
    (provider as any).proxy = false;

    mountA2aOidc(app, provider, { repo, resourceAudience: audience });

    // ── request handler を inline で組む（executor の sleep を制御可能にするため）──
    // executorParked=true の間、sleep は永久に解決しない → executor は submitted を
    // publish した直後に park し、poll でジョブ状態を触らない（reconciler が唯一の収束者）。
    const cardDeps = { baseUrl: base, issuer: `${base}/oidc`, version: '1.0.0-test' };
    const baseCard = buildBaseCard(cardDeps);
    const taskStore = new SqliteA2aTaskStore(repo);
    const parkingSleep = (ms: number): Promise<void> =>
      executorParked
        ? new Promise<void>(() => { /* never resolves — executor parks */ })
        : new Promise<void>(res => setTimeout(res, Math.min(ms, 5)));
    const executor = new MaestroA2aExecutor(repo, {
      pollMs: 5,
      sleep: parkingSleep,
      maxPollIterations: 100,
    });
    const extendedCardProvider = makeExtendedCardProvider(repo, baseCard, cardDeps);
    const handler = new DefaultRequestHandler(
      baseCard, taskStore, executor,
      undefined, undefined, undefined, extendedCardProvider,
    );
    const userBuilder = makeA2aUserBuilder(provider, repo);
    app.use('/a2a', jsonRpcHandler({ requestHandler: handler, userBuilder }));

    // reconciler は interval を張らず、テスト内で reconcileOnce() を明示的に呼ぶ。
    reconciler = new A2aTaskReconciler({ repo });
  });

  afterAll(() => {
    server?.close();
  });

  /** authorize → consent → code → token を実フローで通す。 */
  async function obtainToken(selectedSpaces: string, selectedSkills: string): Promise<string> {
    const client = new Client();
    const verifier = b64url(randomBytes(32));
    const challenge = b64url(createHash('sha256').update(verifier).digest());

    const authUrl = `${base}/oidc/auth?` + new URLSearchParams({
      client_id: clientId, response_type: 'code', redirect_uri: redirectUri,
      scope: 'openid a2a.read', resource: audience,
      code_challenge: challenge, code_challenge_method: 'S256', state: 'xyz',
    });
    const authRes = await client.get(authUrl);
    expect([302, 303]).toContain(authRes.status);
    let loc = authRes.headers.get('location')!;

    for (let i = 0; i < 5; i++) {
      if (loc.startsWith(redirectUri)) break;
      expect(loc).toContain('/oidc/interaction/');
      const uid = new URL(loc, base).pathname.split('/').pop()!;
      const confirmRes = await client.postForm(`${base}/oidc/interaction/${uid}/confirm`, {
        selectedSpaces, selectedSkills,
      });
      expect(confirmRes.status).toBe(200);
      const { redirectTo } = await confirmRes.json() as { redirectTo: string };
      const resumeRes = await client.get(redirectTo);
      expect([302, 303]).toContain(resumeRes.status);
      loc = resumeRes.headers.get('location')!;
    }

    expect(loc.startsWith(redirectUri)).toBe(true);
    const code = new URL(loc).searchParams.get('code');
    expect(code).toBeTruthy();

    const tokenRes = await client.postForm(`${base}/oidc/token`, {
      grant_type: 'authorization_code',
      code: code!, code_verifier: verifier, client_id: clientId, redirect_uri: redirectUri,
    });
    expect(tokenRes.status).toBe(200);
    const tokenJson = await tokenRes.json() as { access_token: string };
    expect(tokenJson.access_token).toBeTruthy();
    return tokenJson.access_token;
  }

  function rpc(body: unknown, headers: Record<string, string> = {}) {
    return fetch(`${base}/a2a`, {
      method: 'POST',
      headers: { 'content-type': 'application/json', ...headers },
      body: JSON.stringify(body),
    });
  }

  function sendEnvelope(
    text: string,
    opts: { blocking?: boolean; metadata?: Record<string, unknown> } = {},
  ) {
    return {
      jsonrpc: '2.0',
      id: 1,
      method: 'message/send',
      params: {
        message: {
          kind: 'message',
          messageId: randomUUID(),
          role: 'user',
          parts: [{ kind: 'text', text }],
          ...(opts.metadata ? { metadata: opts.metadata } : {}),
        },
        ...(opts.blocking !== undefined ? { configuration: { blocking: opts.blocking } } : {}),
      },
    };
  }

  function getTaskEnvelope(id: string) {
    return { jsonrpc: '2.0', id: 2, method: 'tasks/get', params: { id } };
  }

  type TaskResult = {
    kind: string;
    id: string;
    status: { state: string };
    metadata?: Record<string, unknown>;
    artifacts?: Array<{ name?: string; parts: Array<{ kind: string; text?: string }> }>;
  };

  // ── Case 1: 非ブロッキング送信は完了を待たず即座に返る ─────────────────────
  it('blocking:false → returns immediately with a non-terminal Task (does not wait for job)', async () => {
    executorParked = true;
    const token = await obtainToken(spaceId, 'research');

    const res = await rpc(sendEnvelope('do research', { blocking: false }), {
      authorization: `Bearer ${token}`,
    });
    expect(res.status).toBe(200);
    const json = await res.json() as { jsonrpc: string; result?: TaskResult; error?: unknown };

    expect(json.jsonrpc).toBe('2.0');
    expect(json.error).toBeUndefined();
    expect(json.result).toBeDefined();
    expect(json.result!.kind).toBe('task');
    // 完了を待っていない証拠: 非 terminal 状態で早期リターンしている。
    expect(json.result!.status.state).not.toBe('completed');
    expect(['submitted', 'working']).toContain(json.result!.status.state);
    // executor が enqueue した MAESTRO ジョブへのリンクが Task メタデータに載っている。
    expect(typeof json.result!.metadata?.maestroJobId).toBe('string');
  }, 30_000);

  // ── Case 2: job succeeded → reconcileOnce → tasks/get が completed + artifact ──
  it('tasks/get converges to completed via reconciler after job succeeds', async () => {
    executorParked = true; // executor は park 中 → 収束は reconciler のみが行う
    const token = await obtainToken(spaceId, 'research');

    const sendRes = await rpc(sendEnvelope('do research', { blocking: false }), {
      authorization: `Bearer ${token}`,
    });
    const sendJson = await sendRes.json() as { result: TaskResult };
    const taskId = sendJson.result.id;
    const jobId = sendJson.result.metadata!.maestroJobId as string;
    expect(sendJson.result.status.state).not.toBe('completed');

    // ジョブ完了をシミュレート（実 worker の代わり）。
    completeJob(jobId);

    // reconciler が非 terminal タスクを 1 回走査して terminal 化する。
    const { finalized } = await reconciler.reconcileOnce();
    expect(finalized).toBeGreaterThanOrEqual(1);

    // tasks/get で最新状態を取得 → completed + artifact に result.txt の中身。
    const getRes = await rpc(getTaskEnvelope(taskId), { authorization: `Bearer ${token}` });
    expect(getRes.status).toBe(200);
    const getJson = await getRes.json() as { result?: TaskResult; error?: unknown };
    expect(getJson.error).toBeUndefined();
    expect(getJson.result!.status.state).toBe('completed');

    const artifacts = getJson.result!.artifacts ?? [];
    expect(artifacts.length).toBeGreaterThan(0);
    const allText = artifacts.flatMap(a => a.parts).map(p => p.text ?? '').join('\n');
    expect(allText).toContain(RESULT_TEXT);
  }, 30_000);

  // ── Case 3: 再起動シナリオ（executor を通さず reconciler だけで収束）──────────
  it('restart scenario: seeded working task converges via reconcileOnce() alone', async () => {
    // executor を一切通さず、working な a2a_task + 対応する succeeded ジョブを直接仕込む。
    // ＝ブリッジ再起動で live executor が消えた状況の等価物。
    const seededTaskId = `restart-task-${randomUUID()}`;
    const localTaskId = 987654; // 存在しないローカルタスク → reconciler は job.worktreePath にフォールバック

    const job = await repo.createJob({
      repo: localTaskRepoName(localTaskId),
      issueNumber: localTaskId,
      instruction: 'restart research',
      pieceName: 'research',
      ownerId: userId,
      visibility: 'private',
      spaceId,
    });
    completeJob(job.id); // succeeded + worktree_path=outDir

    // シード用の委任行を挿入（reconciler の revocation sweep が fail-closed でキャンセルしないよう live にする）。
    repo.createA2aDelegation({
      id: 'deleg-restart-001',
      userId,
      clientId,
      grantId: 'grant-restart',
      grantedSpaceIds: [spaceId],
      grantedSkills: ['research'],
      audience: null,
      expiresAt: null,
      revokedAt: null,
    });

    repo.saveA2aTask({
      id: seededTaskId,
      contextId: seededTaskId,
      jobId: job.id,
      localTaskId: null,
      payload: {
        id: seededTaskId,
        contextId: seededTaskId,
        kind: 'task',
        status: { state: 'working', timestamp: '2026-07-02T00:00:00.000Z' },
        metadata: {
          maestroJobId: job.id,
          maestroLocalTaskId: null,
          a2aDelegationId: 'deleg-restart-001',
          a2aGrantId: 'grant-restart',
          a2aActingUserId: userId,
        },
      },
      delegationId: 'deleg-restart-001',
      grantId: 'grant-restart',
      actingUserId: userId,
    });

    const { finalized } = await reconciler.reconcileOnce();
    expect(finalized).toBeGreaterThanOrEqual(1);

    const token = await obtainToken(spaceId, 'research');
    const getRes = await rpc(getTaskEnvelope(seededTaskId), { authorization: `Bearer ${token}` });
    const getJson = await getRes.json() as { result?: TaskResult; error?: unknown };
    expect(getJson.error).toBeUndefined();
    expect(getJson.result!.status.state).toBe('completed');
    const allText = (getJson.result!.artifacts ?? [])
      .flatMap(a => a.parts).map(p => p.text ?? '').join('\n');
    expect(allText).toContain(RESULT_TEXT);
  }, 30_000);

  // ── Case 4: blocking:true（既定）は完了まで待って completed を返す（回帰確認）──
  it('blocking:true still blocks to completion and returns a completed Task', async () => {
    executorParked = false; // executor を通常 poll に戻す
    const stopCompleter = startCompleter();
    try {
      const token = await obtainToken(spaceId, 'research');
      const res = await rpc(sendEnvelope('do research', { blocking: true }), {
        authorization: `Bearer ${token}`,
      });
      expect(res.status).toBe(200);
      const json = await res.json() as { result?: TaskResult; error?: unknown };
      expect(json.error).toBeUndefined();
      expect(json.result!.kind).toBe('task');
      expect(json.result!.status.state).toBe('completed');
      const allText = (json.result!.artifacts ?? [])
        .flatMap(a => a.parts).map(p => p.text ?? '').join('\n');
      expect(allText).toContain(RESULT_TEXT);
    } finally {
      stopCompleter();
    }
  }, 30_000);
});
