/**
 * Task 5 — executor in-flight revocation recheck (+ 2C-1 metadata fix).
 *
 * Uses MaestroA2aExecutor directly with injectable sleep/now/pollMs deps,
 * a minimal fake Repository, and a fake ExecutionEventBus — no HTTP server needed.
 *
 * Tests:
 *   A1. Delegation revoked after first poll → executor publishes terminal `canceled`,
 *       calls eventBus.finished(), calls cancelA2aLinkedJob(jobId).
 *   A2. Happy path: live delegation → job completes normally (no premature cancel).
 *   B.  Initial submitted Task metadata: a2aDelegationId = delegation.id,
 *       a2aGrantId = grantId (different values, different semantics).
 */

import { describe, it, expect } from 'vitest';
import { MaestroA2aExecutor } from './executor.js';
import { A2aTaskReconciler } from './reconciler.js';
import type { ExecutionEventBus } from '@a2a-js/sdk/server';
import type { TaskStatusUpdateEvent } from '@a2a-js/sdk';

// ── Fake helpers ──────────────────────────────────────────────────────────────

function makeFakeRepo(opts: {
  delegationId: string;
  grantId: string;
  /** number of getJob calls after which delegation switches to revoked (0 = revoked from the start of poll 1) */
  revokeAfterPollN?: number;
  /** poll number after which job status becomes 'succeeded' */
  succeedAfterPollN?: number;
}) {
  const {
    delegationId,
    grantId,
    revokeAfterPollN = Infinity,
    succeedAfterPollN = Infinity,
  } = opts;

  let pollCount = 0;
  const cancelCalls: string[] = [];

  const baseDelegRow = {
    id: delegationId,
    userId: 'user-1',
    clientId: 'client-1',
    grantId,
    grantedSpaceIds: ['space-1'],
    grantedSkills: ['research'],
    audience: null as string | null,
    expiresAt: null as string | null,
    revokedAt: null as string | null,
  };

  const repo = {
    // computeEffectiveScope → resolveOrgIds needs these
    getUserById: (_id: string) => ({
      id: 'user-1', status: 'active', role: 'user' as const, name: 'Test User',
    }),
    listUserGiteaOrgs: (_userId: string) => [] as Array<{ orgId: string }>,
    listUserLocalOrgs: (_userId: string) => [] as Array<{ orgId: string }>,
    listSpaces: async (_opts: unknown) => [{ id: 'space-1' }],
    getSpaceA2aSkills: (_spaceId: string) => ['research'],

    // Job creation
    createLocalTask: async (_opts: unknown) => ({ id: 1, workspacePath: null }),
    createJob: async (_opts: unknown) => undefined,
    getLatestJobForIssue: async (_repoName: string, _issueNum: number) => ({
      id: 'job-test-1',
      status: 'queued',
      currentActivity: null,
      errorSummary: null,
      abortReason: null,
      worktreePath: null,
    }),
    addAuditLog: async (..._args: unknown[]) => undefined,

    // Poll loop: getJob
    getJob: async (_id: string) => {
      pollCount++;
      const status = pollCount > succeedAfterPollN ? 'succeeded' : 'running';
      return {
        id: 'job-test-1',
        status,
        currentActivity: null,
        errorSummary: null,
        abortReason: null,
        worktreePath: null,
      };
    },

    // Poll loop: revocation recheck (called right after getJob in the same iteration)
    getA2aDelegationByGrantId: (_grantId: string) => {
      // pollCount was just incremented by getJob in the same iteration.
      // After revokeAfterPollN polls, delegation is revoked.
      if (pollCount > revokeAfterPollN) {
        return { ...baseDelegRow, revokedAt: '2026-07-02T00:00:00.000Z' };
      }
      return { ...baseDelegRow };
    },

    // Job cancel — executor uses cancelA2aLinkedJob (covers queued/retry/running/etc.)
    cancelA2aLinkedJob: (jobId: string) => {
      cancelCalls.push(jobId);
      return true;
    },

    // handleTerminal (succeeded path)
    getLocalTask: async (_id: number) => ({ id: 1, workspacePath: null }),
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  } as any;

  return {
    repo,
    cancelCalls,
    getPollCount: () => pollCount,
  };
}

/** Fake eventBus that collects all published events. Uses object reference to avoid stale closure. */
function makeFakeEventBus() {
  const events: unknown[] = [];
  const tracker = { finishedCount: 0 };

  const eventBus: ExecutionEventBus = {
    publish: (event: unknown) => { events.push(event); },
    finished: () => { tracker.finishedCount++; },
  };

  const statusEvents = () =>
    events.filter(
      (e): e is TaskStatusUpdateEvent =>
        typeof e === 'object' &&
        e !== null &&
        (e as Record<string, unknown>).kind === 'status-update',
    );

  return { eventBus, events, tracker, statusEvents };
}

function makeFakeRequestContext(opts: {
  grantId: string;
  delegationId: string;
  taskId?: string;
  contextId?: string;
}) {
  const { grantId, delegationId, taskId = 'a2a-task-1', contextId = 'ctx-1' } = opts;

  const principal = {
    actingUserId: 'user-1',
    clientId: 'client-1',
    grantId,
    delegation: {
      id: delegationId,
      userId: 'user-1',
      clientId: 'client-1',
      grantId,
      grantedSpaceIds: ['space-1'],
      grantedSkills: ['research'],
      expiresAt: null,
      revokedAt: null,
    },
  };

  return {
    taskId,
    contextId,
    context: { user: { a2aPrincipal: principal } },
    userMessage: {
      parts: [{ kind: 'text', text: 'do research' }],
      metadata: { skillId: 'research' },
    },
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  } as any;
}

// ── Tests ─────────────────────────────────────────────────────────────────────

describe('A2A executor revocation recheck (Task 5)', () => {
  // Fixed clock for determinism
  const nowFn = () => '2026-07-02T00:00:00.000Z';

  it('A1: revoked delegation after first poll → executor cancels and publishes terminal canceled', async () => {
    const GRANT_ID = 'grant-revoke-test';
    const DELEG_ID = 'deleg-pk-001';

    const { repo, cancelCalls } = makeFakeRepo({
      delegationId: DELEG_ID,
      grantId: GRANT_ID,
      // delegation is live for poll #1 (pollCount=1 <= 1),
      // revoked starting poll #2 (pollCount=2 > 1)
      revokeAfterPollN: 1,
    });
    const { eventBus, tracker, statusEvents } = makeFakeEventBus();
    const requestContext = makeFakeRequestContext({ grantId: GRANT_ID, delegationId: DELEG_ID });

    const executor = new MaestroA2aExecutor(repo, {
      pollMs: 0,
      sleep: async () => {},
      now: nowFn,
      maxPollIterations: 10,
    });

    await executor.execute(requestContext, eventBus);

    // Must have called cancelA2aLinkedJob (covers all non-terminal statuses including retry)
    expect(cancelCalls).toContain('job-test-1');

    // Must have published a terminal 'canceled' status update
    const allStatusEvents = statusEvents();
    const terminalEvents = allStatusEvents.filter(e => e.final === true);
    expect(terminalEvents.length).toBeGreaterThanOrEqual(1);
    const canceledEvent = terminalEvents.find(e => (e.status as Record<string, unknown>)?.state === 'canceled');
    expect(canceledEvent).toBeDefined();
    expect((canceledEvent!.status as Record<string, unknown>).state).toBe('canceled');

    // Must have called eventBus.finished() exactly once
    expect(tracker.finishedCount).toBe(1);
  });

  it('A2: live delegation throughout → job completes normally without premature cancel', async () => {
    const GRANT_ID = 'grant-live-test';
    const DELEG_ID = 'deleg-pk-002';

    const { repo, cancelCalls } = makeFakeRepo({
      delegationId: DELEG_ID,
      grantId: GRANT_ID,
      revokeAfterPollN: Infinity, // delegation stays live forever
      succeedAfterPollN: 2,        // job succeeds after 2 polls
    });
    const { eventBus, tracker, statusEvents } = makeFakeEventBus();
    const requestContext = makeFakeRequestContext({ grantId: GRANT_ID, delegationId: DELEG_ID });

    const executor = new MaestroA2aExecutor(repo, {
      pollMs: 0,
      sleep: async () => {},
      now: nowFn,
      maxPollIterations: 10,
    });

    await executor.execute(requestContext, eventBus);

    // Must NOT have called cancelA2aLinkedJob — delegation is still live
    expect(cancelCalls).toHaveLength(0);

    // Must have published 'completed' terminal event (not 'canceled')
    const terminalEvents = statusEvents().filter(e => e.final === true);
    expect(terminalEvents.length).toBeGreaterThanOrEqual(1);
    const completedEvent = terminalEvents.find(
      e => (e.status as Record<string, unknown>)?.state === 'completed',
    );
    expect(completedEvent).toBeDefined();
    expect((completedEvent!.status as Record<string, unknown>).state).toBe('completed');

    // No 'canceled' event in the terminal events
    const canceledEvent = terminalEvents.find(
      e => (e.status as Record<string, unknown>)?.state === 'canceled',
    );
    expect(canceledEvent).toBeUndefined();

    // eventBus.finished() must have been called exactly once
    expect(tracker.finishedCount).toBe(1);
  });

  it('B: initial submitted Task metadata has a2aDelegationId = delegation.id and a2aGrantId = grantId', async () => {
    const GRANT_ID = 'grant-metadata-test';
    const DELEG_ID = 'deleg-pk-real-003'; // deliberately different from GRANT_ID

    const { repo } = makeFakeRepo({
      delegationId: DELEG_ID,
      grantId: GRANT_ID,
      // revoke immediately so execute() exits quickly after the first poll
      revokeAfterPollN: 0,
    });
    const { eventBus, events } = makeFakeEventBus();
    const requestContext = makeFakeRequestContext({ grantId: GRANT_ID, delegationId: DELEG_ID });

    const executor = new MaestroA2aExecutor(repo, {
      pollMs: 0,
      sleep: async () => {},
      now: nowFn,
      maxPollIterations: 10,
    });

    await executor.execute(requestContext, eventBus);

    // The first published event must be the initial 'submitted' Task (kind: 'task')
    const taskEvents = events.filter(
      (e): e is Record<string, unknown> =>
        typeof e === 'object' && e !== null && (e as Record<string, unknown>).kind === 'task',
    );
    expect(taskEvents.length).toBeGreaterThanOrEqual(1);

    const initialTask = taskEvents[0];
    const metadata = initialTask.metadata as Record<string, unknown>;

    // a2aDelegationId must equal the real delegation PK (not grantId)
    expect(metadata.a2aDelegationId).toBe(DELEG_ID);
    // a2aGrantId must equal the OAuth grant id
    expect(metadata.a2aGrantId).toBe(GRANT_ID);
    // Both values must differ — verifies we're not using grantId for both
    expect(metadata.a2aDelegationId).not.toBe(metadata.a2aGrantId);
  });
});

// ── Task 6: Reconciler revocation sweep helpers ───────────────────────────────

const RECONCILER_TERMINAL_STATES = new Set(['completed', 'failed', 'canceled', 'rejected']);

function makeReconcilerFakeRepo(opts: {
  grantId: string;
  delegationRevoked: boolean;
  initialJobStatus?: string;
  initialTaskState?: string;
}) {
  let currentJobStatus = opts.initialJobStatus ?? 'running';
  let currentPayload: Record<string, unknown> = {
    contextId: 'ctx-r',
    status: { state: opts.initialTaskState ?? 'working' },
    metadata: { a2aGrantId: opts.grantId },
  };
  const cancelCalls: string[] = [];

  const delegRow = {
    id: 'deleg-r-001',
    userId: 'user-1',
    clientId: 'client-1',
    grantId: opts.grantId,
    grantedSpaceIds: ['space-1'],
    grantedSkills: ['research'],
    audience: null as string | null,
    expiresAt: null as string | null,
    revokedAt: opts.delegationRevoked ? '2026-07-01T00:00:00.000Z' : null as string | null,
  };

  const repo = {
    listNonTerminalA2aTasks: (_limit: number) => {
      const state = (currentPayload.status as Record<string, unknown> | undefined)?.state as string | undefined;
      if (state && RECONCILER_TERMINAL_STATES.has(state)) return [];
      return [{ id: 'a2a-task-r1', jobId: 'job-r1', payload: { ...currentPayload } }];
    },
    getA2aDelegationByGrantId: (_grantId: string) => delegRow,
    cancelA2aLinkedJob: (jobId: string) => {
      cancelCalls.push(jobId);
      currentJobStatus = 'cancelled';
      return true;
    },
    saveA2aTask: (row: { payload: object }) => {
      currentPayload = { ...(row.payload as Record<string, unknown>) };
    },
    getJob: async (_id: string) => ({
      id: 'job-r1',
      status: currentJobStatus,
      currentActivity: null,
      errorSummary: null,
      abortReason: null,
      worktreePath: null,
    }),
    getLocalTask: async (id: number) => ({ id, workspacePath: null }),
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  } as any;

  return {
    repo,
    cancelCalls,
    getCurrentPayload: () => currentPayload,
    getCurrentJobStatus: () => currentJobStatus,
  };
}

// ── Task 6: Reconciler revocation sweep tests ─────────────────────────────────

describe('A2A reconciler revocation sweep (Task 6)', () => {
  const nowFn = () => '2026-07-02T00:00:00.000Z';

  it('R1: revoked delegation + running linked job → job cancelled and task finalized to canceled', async () => {
    const GRANT_ID = 'grant-revoked-r1';
    const { repo, cancelCalls, getCurrentPayload } = makeReconcilerFakeRepo({
      grantId: GRANT_ID,
      delegationRevoked: true,
      initialJobStatus: 'running',
    });

    const reconciler = new A2aTaskReconciler({ repo, now: nowFn });
    const result = await reconciler.reconcileOnce();

    // Job must have been cancelled via cancelA2aLinkedJob
    expect(cancelCalls).toContain('job-r1');

    // Task payload must be finalized to A2A 'canceled' state
    const savedState = (getCurrentPayload().status as Record<string, unknown>)?.state;
    expect(savedState).toBe('canceled');

    // Reconciler must report at least one finalization
    expect(result.finalized).toBeGreaterThanOrEqual(1);
  });

  it('R2: live delegation → task left untouched (still working, job not cancelled)', async () => {
    const GRANT_ID = 'grant-live-r2';
    const { repo, cancelCalls, getCurrentPayload } = makeReconcilerFakeRepo({
      grantId: GRANT_ID,
      delegationRevoked: false,
      initialJobStatus: 'running',
    });

    const reconciler = new A2aTaskReconciler({ repo, now: nowFn });
    await reconciler.reconcileOnce();

    // cancelA2aLinkedJob must NOT have been called
    expect(cancelCalls).toHaveLength(0);

    // Task must remain in its original non-terminal state (saveA2aTask was not called)
    const state = (getCurrentPayload().status as Record<string, unknown>)?.state;
    expect(state).toBe('working');
  });

  it('R3: idempotency — second reconcileOnce() after R1 makes no further changes', async () => {
    const GRANT_ID = 'grant-revoked-r3';
    const { repo, cancelCalls, getCurrentPayload } = makeReconcilerFakeRepo({
      grantId: GRANT_ID,
      delegationRevoked: true,
      initialJobStatus: 'running',
    });

    const reconciler = new A2aTaskReconciler({ repo, now: nowFn });

    // First run — equivalent to R1
    await reconciler.reconcileOnce();
    expect(cancelCalls).toHaveLength(1);
    expect((getCurrentPayload().status as Record<string, unknown>)?.state).toBe('canceled');

    // Second run — task is now terminal so listNonTerminalA2aTasks returns nothing
    const result = await reconciler.reconcileOnce();

    // No additional cancel calls
    expect(cancelCalls).toHaveLength(1);
    // Payload stays canceled
    expect((getCurrentPayload().status as Record<string, unknown>)?.state).toBe('canceled');
    // No new rows scanned or finalized
    expect(result.finalized).toBe(0);
  });
});
