// @vitest-environment node
import { describe, it, expect } from 'vitest';
import express from 'express';
import request from 'supertest';
import { Repository } from '../../db/repository.js';
import { createConsentRouter } from './consent-api.js';

const RESOURCE_AUDIENCE = 'https://m/a2a';

function fakeProvider(captured: any, overrideParams?: Record<string, unknown>) {
  class Grant {
    constructor(public a: any) { captured.grant = { ...a, scopes: [] as string[], resources: {} as any }; }
    addOIDCScope(s: string) { captured.grant.scopes.push(s); }
    addResourceScope(r: string, s: string) { captured.grant.resources[r] = s; }
    async save() { return 'grant-xyz'; }
  }
  const params = overrideParams ?? { client_id: 'cli1', resource: RESOURCE_AUDIENCE, scope: 'openid a2a.read' };
  return {
    Grant,
    async interactionDetails() { return { uid: 'u1', params }; },
    async interactionResult() { return '/oidc/auth/u1'; },
  } as any;
}

function fakeProviderAbortThrows() {
  class Grant {
    constructor(public a: any) {}
    addOIDCScope() {}
    addResourceScope() {}
    async save() { return 'grant-xyz'; }
  }
  return {
    Grant,
    async interactionDetails() { return { uid: 'u1', params: { client_id: 'cli1', scope: 'openid a2a.read' } }; },
    async interactionResult() { throw new Error('SessionNotFound'); },
  } as any;
}

describe('consent abort', () => {
  it('returns 500 and does NOT crash when interactionResult rejects (SessionNotFound)', async () => {
    const repo = new Repository(':memory:');
    const app = express();
    app.use(express.json());
    app.use('/oidc/interaction', createConsentRouter(fakeProviderAbortThrows(), repo, RESOURCE_AUDIENCE));
    const res = await request(app).post('/oidc/interaction/u1/abort').send({});
    expect(res.status).toBe(500);
    expect(res.body.error).toBe('abort error');
  });
});

describe('consent confirm', () => {
  it('saves a grant with the resource scope, binds accountId from session user, and audit-logs it', async () => {
    const repo = new Repository(':memory:');
    const captured: any = {};
    const app = express();
    app.use(express.json());
    app.use((req, _res, next) => { (req as any).user = { id: 'user-9', role: 'user' }; next(); });
    app.use('/oidc/interaction', createConsentRouter(fakeProvider(captured), repo, RESOURCE_AUDIENCE));
    const res = await request(app).post('/oidc/interaction/u1/confirm').send({});
    expect(res.status).toBe(200);
    expect(res.body.redirectTo).toBe('/oidc/auth/u1');
    // accountId must come from the session user, not from any client param (IDOR guard)
    expect(captured.grant.accountId).toBe('user-9');
    // resource scope must be recorded
    expect(captured.grant.resources[RESOURCE_AUDIENCE]).toBe('a2a.read');
    // audit_log row must have been written
    const rows = (repo as any).db.prepare("SELECT action, actor FROM audit_log WHERE action='a2a.consent.granted'").all();
    expect(rows).toHaveLength(1);
    expect(rows[0].actor).toBe('user-9');
  });

  it('defaults to resourceAudience when client omits resource param', async () => {
    const repo = new Repository(':memory:');
    const captured: any = {};
    const app = express();
    app.use(express.json());
    app.use((req, _res, next) => { (req as any).user = { id: 'user-9', role: 'user' }; next(); });
    // params WITHOUT a resource key
    const paramsNoResource = { client_id: 'cli1', scope: 'openid a2a.read' };
    app.use('/oidc/interaction', createConsentRouter(fakeProvider(captured, paramsNoResource), repo, RESOURCE_AUDIENCE));
    const res = await request(app).post('/oidc/interaction/u1/confirm').send({});
    expect(res.status).toBe(200);
    // must fall back to the configured audience
    expect(captured.grant.resources[RESOURCE_AUDIENCE]).toBe('a2a.read');
  });

  it('creates an a2a_delegations row with server-validated spaces/skills and drops client-sent skills not in allowlist', async () => {
    const repo = new Repository(':memory:');
    // Seed a space owned by the test user with two allowlisted skills.
    (repo as any).db.prepare(
      "INSERT INTO spaces (id, kind, title, owner_id) VALUES ('sp1', 'case', 'My Space', 'user-9')"
    ).run();
    repo.setSpaceA2aSkills('sp1', ['skill-a', 'skill-b']);

    const captured: any = {};
    const app = express();
    app.use(express.json());
    app.use((req, _res, next) => { (req as any).user = { id: 'user-9', role: 'user' }; next(); });
    app.use('/oidc/interaction', createConsentRouter(fakeProvider(captured), repo, RESOURCE_AUDIENCE));

    // Client selects sp1 + skill-a + a bad skill not in the allowlist.
    const res = await request(app).post('/oidc/interaction/u1/confirm').send({
      selectedSpaces: ['sp1'],
      selectedSkills: ['skill-a', 'not-allowlisted-skill'],
    });
    expect(res.status).toBe(200);

    // The delegation must exist, linked to the grantId returned by grant.save().
    const delegation = repo.getA2aDelegationByGrantId('grant-xyz');
    expect(delegation).toBeDefined();
    expect(delegation!.grantedSpaceIds).toEqual(['sp1']);
    // 'not-allowlisted-skill' must have been dropped; 'skill-a' kept.
    expect(delegation!.grantedSkills).toEqual(['skill-a']);
    expect(delegation!.userId).toBe('user-9');
    expect(delegation!.clientId).toBe('cli1');
  });

  it('drops a space the consent user cannot see (private space owned by another user)', async () => {
    const repo = new Repository(':memory:');
    // sp-own: owned by user-9 (visible to user-9)
    (repo as any).db.prepare(
      "INSERT INTO spaces (id, kind, title, owner_id) VALUES ('sp-own', 'case', 'My Space', 'user-9')"
    ).run();
    repo.setSpaceA2aSkills('sp-own', ['skill-x']);

    // sp-other: owned by a different user, private (NOT visible to user-9)
    (repo as any).db.prepare(
      "INSERT INTO spaces (id, kind, title, owner_id) VALUES ('sp-other', 'case', 'Other Space', 'user-other')"
    ).run();
    repo.setSpaceA2aSkills('sp-other', ['skill-y']);

    const captured: any = {};
    const app = express();
    // intentionally omit app-level express.json() to prove the router self-mounts body parsers
    app.use((req, _res, next) => { (req as any).user = { id: 'user-9', role: 'user' }; next(); });
    app.use('/oidc/interaction', createConsentRouter(fakeProvider(captured), repo, RESOURCE_AUDIENCE));

    // user selects BOTH spaces
    const res = await request(app)
      .post('/oidc/interaction/u1/confirm')
      .type('form')
      .send('selectedSpaces=sp-own&selectedSpaces=sp-other&selectedSkills=skill-x&selectedSkills=skill-y');
    expect(res.status).toBe(200);

    const delegation = repo.getA2aDelegationByGrantId('grant-xyz');
    expect(delegation).toBeDefined();
    // sp-other must have been dropped by listSpaces({viewer}) visibility check
    expect(delegation!.grantedSpaceIds).toEqual(['sp-own']);
    expect(delegation!.grantedSkills).toEqual(['skill-x']);
  });
});
