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 { GetController } from '../get.controller.js';
import type { FederationGetQueryResult } from '../get-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: 1,
  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?: FederationGetQueryResult;
}) {
  const scope = {
    evaluateAccess: vi.fn().mockResolvedValue(opts?.scopeResult ?? allowedScope()),
  };
  const query = {
    evaluateReadAccess: vi.fn(),
    get: vi.fn().mockResolvedValue(
      opts?.queryResult ?? {
        status: 'found',
        item: {
          id: 'task-1',
          title: 'Federated task',
          createdAt: new Date('2026-06-24T00:00:00.000Z'),
        },
      },
    ),
  };

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

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

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

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

    const response = await controller.get('tasks', 'task-1', makeRequest());

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

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

    await expect(
      controller.get('tasks', 'task-1', {} as unknown as FastifyRequest),
    ).rejects.toMatchObject({
      response: {
        error: {
          code: 'unauthorized',
          message: 'Federation context missing',
        },
      },
      status: 401,
    });
    expect(scope.evaluateAccess).not.toHaveBeenCalled();
    expect(query.get).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.get('credentials', 'cred-1', makeRequest())).rejects.toMatchObject({
      response: {
        error: {
          code: 'scope_violation',
          message: 'Requested federation resource is explicitly excluded by grant scope',
        },
      },
      status: 403,
    });
    expect(query.get).not.toHaveBeenCalled();
  });

  it('returns 404 when the scoped query layer cannot find the resource id', async () => {
    const { controller } = makeController({ queryResult: { status: 'not_found' } });

    await expect(controller.get('tasks', 'missing-task', makeRequest())).rejects.toMatchObject({
      response: { error: { code: 'not_found' } },
      status: 404,
    });
  });

  it('returns 403 when the resource exists outside the RBAC/scope intersection', async () => {
    const { controller } = makeController({
      queryResult: { status: 'denied', reason: 'Task is outside the federated scope' },
    });

    await expect(controller.get('tasks', 'task-2', makeRequest())).rejects.toMatchObject({
      response: {
        error: {
          code: 'scope_violation',
          message: 'Task is outside the federated scope',
        },
      },
      status: 403,
    });
  });

  it('fails closed when the query layer denies an unsupported resource', async () => {
    const unsupportedFilter: FederationScopeQueryFilter = {
      ...TASK_FILTER,
      resource: 'unknown-resource' as FederationScopeQueryFilter['resource'],
    };
    const { controller } = makeController({
      scopeResult: allowedScope(unsupportedFilter),
      queryResult: {
        status: 'denied',
        reason: 'Unsupported federation get resource: unknown-resource',
      },
    });

    await expect(controller.get('unknown-resource', 'row-1', makeRequest())).rejects.toMatchObject({
      response: {
        error: {
          code: 'scope_violation',
          message: 'Unsupported federation get resource: unknown-resource',
        },
      },
      status: 403,
    });
  });

  it('rejects empty ids before evaluating scope', async () => {
    const { controller, scope, query } = makeController();

    await expect(controller.get('tasks', '   ', makeRequest())).rejects.toMatchObject({
      response: { error: { code: 'invalid_request' } },
      status: 400,
    });
    expect(scope.evaluateAccess).not.toHaveBeenCalled();
    expect(query.get).not.toHaveBeenCalled();
  });
});
