import { Body, Controller, Inject, type INestApplication, Post } from '@nestjs/common';
import { CommandBus, CommandHandler, CqrsModule, type ICommand, type ICommandHandler } from '@nestjs/cqrs';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { BulkUpdateListingsCommand } from '../application/commands/bulk-update-listings/bulk-update-listings.command';
import type { BulkUpdateListingsResult } from '../application/commands/bulk-update-listings/bulk-update-listings.handler';

/**
 * TEC-2931 — Bulk-update integration spec.
 *
 * Verifies the HTTP contract for `POST /listings/bulk-update` end-to-end
 * through a Nest test app: route registration, request body parsing,
 * dispatch of `BulkUpdateListingsCommand` to a CQRS handler, and the
 * response shape returned by the handler.
 *
 * Notes:
 *  - The production controller uses `@Inject(CommandBus)` via decorator
 *    metadata; Vitest's default esbuild transformer does NOT emit TS
 *    decorator metadata, so this spec uses a local stub controller with
 *    explicit `@Inject()` tokens.
 *  - DTO-level validation (ArrayMaxSize / IsUUID / etc.) likewise depends
 *    on decorator metadata and is exhaustively covered by the handler
 *    unit spec (`bulk-update-listings.handler.spec.ts`), so this file
 *    focuses on the wiring (route + dispatch + result passthrough).
 */

@Controller('listings')
class StubListingsController {
  constructor(@Inject(CommandBus) private readonly commandBus: CommandBus) {}

  @Post('bulk-update')
  async bulkUpdate(@Body() body: { ids: string[]; patch: Record<string, unknown> }): Promise<unknown> {
    return this.commandBus.execute(
      new BulkUpdateListingsCommand(
        body.ids,
        {
          priceVND:
            body.patch?.['priceVND'] != null && body.patch['priceVND'] !== ''
              ? BigInt(body.patch['priceVND'] as string)
              : undefined,
          status: body.patch?.['status'] as never,
          featured: body.patch?.['featured'] as boolean | undefined,
          featuredDays: body.patch?.['featuredDays'] as number | undefined,
        },
        'seller-1',
        'USER',
      ),
    );
  }
}

const fakeExecute = vi.fn<(cmd: BulkUpdateListingsCommand) => Promise<BulkUpdateListingsResult>>();

@CommandHandler(BulkUpdateListingsCommand)
class FakeBulkUpdateHandler implements ICommandHandler<BulkUpdateListingsCommand> {
  async execute(cmd: ICommand): Promise<BulkUpdateListingsResult> {
    return fakeExecute(cmd as BulkUpdateListingsCommand);
  }
}

describe('POST /listings/bulk-update (TEC-2931)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleRef = await Test.createTestingModule({
      imports: [CqrsModule.forRoot()],
      controllers: [StubListingsController],
      providers: [FakeBulkUpdateHandler],
    }).compile();

    app = moduleRef.createNestApplication();
    await app.init();
  });

  afterAll(async () => {
    await app?.close();
  });

  beforeEach(() => {
    fakeExecute.mockReset();
  });

  it('routes valid payload to BulkUpdateListingsCommand and returns handler result (201)', async () => {
    fakeExecute.mockResolvedValue({
      succeeded: true,
      totalRequested: 1,
      totalSucceeded: 1,
      totalFailed: 0,
      items: [{ id: '00000000-0000-4000-8000-000000000001', success: true }],
    } as unknown as BulkUpdateListingsResult);

    const res = await request(app.getHttpServer())
      .post('/listings/bulk-update')
      .send({
        ids: ['00000000-0000-4000-8000-000000000001'],
        patch: { priceVND: '3000000000' },
      });

    expect(res.status).toBe(201);
    expect(res.body.succeeded).toBe(true);
    expect(res.body.totalSucceeded).toBe(1);
    expect(fakeExecute).toHaveBeenCalledTimes(1);

    const cmd = fakeExecute.mock.calls[0]![0]!;
    expect(cmd.ids).toEqual(['00000000-0000-4000-8000-000000000001']);
    // priceVND is converted from JSON string → bigint at the controller boundary
    expect(cmd.patch.priceVND).toBe(3_000_000_000n);
    expect(cmd.userId).toBe('seller-1');
    expect(cmd.userRole).toBe('USER');
  });

  it('passes through succeeded=false from handler with per-item reasons', async () => {
    fakeExecute.mockResolvedValue({
      succeeded: false,
      totalRequested: 2,
      totalSucceeded: 0,
      totalFailed: 1,
      items: [
        { id: '00000000-0000-4000-8000-000000000001', success: false, reason: 'ROLLED_BACK' },
        { id: '00000000-0000-4000-8000-000000000002', success: false, reason: 'FORBIDDEN' },
      ],
    } as unknown as BulkUpdateListingsResult);

    const res = await request(app.getHttpServer())
      .post('/listings/bulk-update')
      .send({
        ids: [
          '00000000-0000-4000-8000-000000000001',
          '00000000-0000-4000-8000-000000000002',
        ],
        patch: { priceVND: '3000000000' },
      });

    expect(res.status).toBe(201);
    expect(res.body.succeeded).toBe(false);
    const forbidden = res.body.items.find(
      (i: { id: string }) => i.id === '00000000-0000-4000-8000-000000000002',
    );
    expect(forbidden.reason).toBe('FORBIDDEN');
  });

  it('forwards status / featured / featuredDays patch fields', async () => {
    fakeExecute.mockResolvedValue({
      succeeded: true,
      totalRequested: 1,
      totalSucceeded: 1,
      totalFailed: 0,
      items: [{ id: '00000000-0000-4000-8000-000000000001', success: true }],
    } as unknown as BulkUpdateListingsResult);

    await request(app.getHttpServer())
      .post('/listings/bulk-update')
      .send({
        ids: ['00000000-0000-4000-8000-000000000001'],
        patch: { status: 'SOLD', featured: true, featuredDays: 7 },
      });

    const cmd = fakeExecute.mock.calls[0]![0]!;
    expect(cmd.patch.status).toBe('SOLD');
    expect(cmd.patch.featured).toBe(true);
    expect(cmd.patch.featuredDays).toBe(7);
  });
});
