import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
import { UpdatePaymentStatusUseCase, type UpdatePaymentStatusInput } from './UpdatePaymentStatusUseCase';
import type { IPaymentRepository } from '../../domain/repositories/IPaymentRepository';
import { PaymentStatus, PaymentType, PayerType, type Payment } from '../../domain/entities/Payment';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';

describe('UpdatePaymentStatusUseCase', () => {
  let paymentRepository: {
    findById: Mock;
    update: Mock;
  };

  let output: {
    present: Mock;
  };

  let useCase: UpdatePaymentStatusUseCase;

  beforeEach(() => {
    paymentRepository = {
      findById: vi.fn(),
      update: vi.fn(),
    };

    output = {
      present: vi.fn(),
    };

    useCase = new UpdatePaymentStatusUseCase(
      paymentRepository as unknown as IPaymentRepository,
      output as unknown as UseCaseOutputPort<unknown>,
    );
  });

  it('returns PAYMENT_NOT_FOUND when payment does not exist', async () => {
    const input: UpdatePaymentStatusInput = {
      paymentId: 'payment-1',
      status: PaymentStatus.COMPLETED,
    };

    paymentRepository.findById.mockResolvedValue(null);

    const result = await useCase.execute(input);

    expect(result.isErr()).toBe(true);
    expect(result.unwrapErr().code).toBe('PAYMENT_NOT_FOUND');
    expect(paymentRepository.update).not.toHaveBeenCalled();
    expect(output.present).not.toHaveBeenCalled();
  });

  it('sets completedAt when status becomes COMPLETED', async () => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date('2025-01-01T00:00:00.000Z'));

    try {
      const input: UpdatePaymentStatusInput = {
        paymentId: 'payment-1',
        status: PaymentStatus.COMPLETED,
      };

      const existingPayment: Payment = {
        id: 'payment-1',
        type: PaymentType.SPONSORSHIP,
        amount: 100,
        platformFee: 5,
        netAmount: 95,
        payerId: 'payer-1',
        payerType: PayerType.SPONSOR,
        leagueId: 'league-1',
        status: PaymentStatus.PENDING,
        createdAt: new Date('2024-12-31T00:00:00.000Z'),
      };

      paymentRepository.findById.mockResolvedValue(existingPayment);
      paymentRepository.update.mockImplementation(async (p: Payment) => ({ ...p }));

      const result = await useCase.execute(input);

      expect(result.isOk()).toBe(true);

      expect(paymentRepository.update).toHaveBeenCalledWith(
        expect.objectContaining({
          id: 'payment-1',
          status: PaymentStatus.COMPLETED,
          completedAt: new Date('2025-01-01T00:00:00.000Z'),
        }),
      );

      const savedPayment = paymentRepository.update.mock.results[0]?.value;
      await expect(savedPayment).resolves.toEqual(
        expect.objectContaining({
          id: 'payment-1',
          status: PaymentStatus.COMPLETED,
          completedAt: new Date('2025-01-01T00:00:00.000Z'),
        }),
      );

      const presentedPayment = (output.present.mock.calls[0]?.[0] as { payment: Payment }).payment;
      expect(presentedPayment.status).toBe(PaymentStatus.COMPLETED);
      expect(presentedPayment.completedAt).toEqual(new Date('2025-01-01T00:00:00.000Z'));
    } finally {
      vi.useRealTimers();
    }
  });

  it('preserves completedAt when status is not COMPLETED', async () => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));

    try {
      const input: UpdatePaymentStatusInput = {
        paymentId: 'payment-1',
        status: PaymentStatus.FAILED,
      };

      const existingCompletedAt = new Date('2025-01-01T00:00:00.000Z');

      const existingPayment: Payment = {
        id: 'payment-1',
        type: PaymentType.SPONSORSHIP,
        amount: 100,
        platformFee: 5,
        netAmount: 95,
        payerId: 'payer-1',
        payerType: PayerType.SPONSOR,
        leagueId: 'league-1',
        status: PaymentStatus.COMPLETED,
        createdAt: new Date('2024-12-31T00:00:00.000Z'),
        completedAt: existingCompletedAt,
      };

      paymentRepository.findById.mockResolvedValue(existingPayment);
      paymentRepository.update.mockImplementation(async (p: Payment) => ({ ...p }));

      const result = await useCase.execute(input);

      expect(result.isOk()).toBe(true);

      expect(paymentRepository.update).toHaveBeenCalledWith(
        expect.objectContaining({
          id: 'payment-1',
          status: PaymentStatus.FAILED,
          completedAt: existingCompletedAt,
        }),
      );

      const presentedPayment = (output.present.mock.calls[0]?.[0] as { payment: Payment }).payment;
      expect(presentedPayment.status).toBe(PaymentStatus.FAILED);
      expect(presentedPayment.completedAt).toEqual(existingCompletedAt);
    } finally {
      vi.useRealTimers();
    }
  });
});