import 'reflect-metadata';
import { RequestMethod } from '@nestjs/common';
import type { FastifyRequest } from 'fastify';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { FederationAuthGuard } from '../../federation-auth.guard.js';
import type {
  FederationScopeEvaluationResult,
  FederationScopeQueryFilter,
} from '../../scope.service.js';
import { ListController } from '../list.controller.js';
import type { FederationListQueryResult } from '../list-query.service.js';

const FEDERATION_CONTEXT = {
  grantId: 'grant-1',
  peerId: 'peer-1',
  subjectUserId: 'user-1',
  scope: { resources: ['tasks'], max_rows_per_query: 25 },
};

const TASK_FILTER: FederationScopeQueryFilter = {
  resource: 'tasks',
  subjectUserId: 'user-1',
  includePersonal: true,
  teamIds: ['team-1'],
  limit: 10,
  maxRowsPerQuery: 25,
};

function makeRequest(): FastifyRequest {
  return { federationContext: FEDERATION_CONTEXT } as unknown as FastifyRequest;
}

function allowedScope(
  filter: FederationScopeQueryFilter = TASK_FILTER,
): FederationScopeEvaluationResult {
  return { allowed: true, filter };
}

function makeController(opts?: {
  scopeResult?: FederationScopeEvaluationResult;
  queryResult?: FederationListQueryResult;
}) {
  const scope = {
    evaluateAccess: vi.fn().mockResolvedValue(opts?.scopeResult ?? allowedScope()),
  };
  const query = {
    evaluateReadAccess: vi.fn(),
    list: vi.fn().mockResolvedValue(
      opts?.queryResult ?? {
        items: [
          {
            id: 'task-1',
            title: 'Federated task',
            createdAt: new Date('2026-06-24T00:00:00.000Z'),
          },
        ],
        truncated: false,
      },
    ),
  };

  return {
    controller: new ListController(scope as never, query as never),
    scope,
    query,
  };
}

describe('ListController', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('declares POST /api/federation/v1/list/:resource protected only by FederationAuthGuard', () => {
    expect(Reflect.getMetadata('path', ListController)).toBe('api/federation/v1/list');
    expect(Reflect.getMetadata('path', ListController.prototype.list)).toBe(':resource');
    expect(Reflect.getMetadata('method', ListController.prototype.list)).toBe(RequestMethod.POST);
    expect(Reflect.getMetadata('__guards__', ListController)).toEqual([FederationAuthGuard]);
  });

  it('runs AuthGuard context through ScopeService and returns local-source tagged rows', async () => {
    const { controller, scope, query } = makeController();

    const response = await controller.list('tasks', makeRequest(), { limit: 10 });

    expect(scope.evaluateAccess).toHaveBeenCalledWith({
      context: FEDERATION_CONTEXT,
      resource: 'tasks',
      requestedLimit: 10,
      nativeRbac: query,
    });
    expect(query.list).toHaveBeenCalledWith({ filter: TASK_FILTER, cursor: undefined });
    expect(response).toEqual({
      items: [
        {
          id: 'task-1',
          title: 'Federated task',
          createdAt: new Date('2026-06-24T00:00:00.000Z'),
          _source: 'local',
        },
      ],
    });
  });

  it('preserves pagination metadata when row cap truncates the query layer result', async () => {
    const { controller } = makeController({
      queryResult: {
        items: [{ id: 'task-1' }],
        nextCursor: 'cursor-2',
        truncated: true,
      },
    });

    const response = await controller.list('tasks', makeRequest(), { cursor: 'cursor-1' });

    expect(response).toEqual({
      items: [{ id: 'task-1', _source: 'local' }],
      nextCursor: 'cursor-2',
      _truncated: true,
    });
  });

  it('returns a federation error envelope when auth guard context is missing', async () => {
    const { controller, scope, query } = makeController();

    await expect(
      controller.list('tasks', {} as unknown as FastifyRequest, {}),
    ).rejects.toMatchObject({
      response: {
        error: {
          code: 'unauthorized',
          message: 'Federation context missing',
        },
      },
      status: 401,
    });
    expect(scope.evaluateAccess).not.toHaveBeenCalled();
    expect(query.list).not.toHaveBeenCalled();
  });

  it('returns a federation error envelope when scope evaluation denies access', async () => {
    const { controller, query } = makeController({
      scopeResult: {
        allowed: false,
        deny: {
          code: 'resource_excluded',
          stage: 'resource_exclusion',
          statusCode: 403,
          message: 'Requested federation resource is explicitly excluded by grant scope',
          grantId: 'grant-1',
          peerId: 'peer-1',
          subjectUserId: 'user-1',
          resource: 'credentials',
        },
      },
    });

    await expect(controller.list('credentials', makeRequest(), {})).rejects.toMatchObject({
      response: {
        error: {
          code: 'scope_violation',
          message: 'Requested federation resource is explicitly excluded by grant scope',
        },
      },
      status: 403,
    });
    expect(query.list).not.toHaveBeenCalled();
  });

  it('rejects malformed request body fields before querying storage', async () => {
    const { controller, scope, query } = makeController();

    await expect(controller.list('tasks', makeRequest(), { cursor: 123 })).rejects.toMatchObject({
      response: { error: { code: 'invalid_request' } },
      status: 400,
    });
    await expect(controller.list('tasks', makeRequest(), { limit: false })).rejects.toMatchObject({
      response: { error: { code: 'invalid_request' } },
      status: 400,
    });
    await expect(controller.list('tasks', makeRequest(), { limit: 'abc' })).rejects.toMatchObject({
      response: { error: { code: 'invalid_request' } },
      status: 400,
    });
    expect(scope.evaluateAccess).not.toHaveBeenCalled();
    expect(query.list).not.toHaveBeenCalled();
  });
});
