import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import type { Logger, UseCaseOutputPort } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import {
  RejectSponsorshipRequestUseCase,
  type RejectSponsorshipRequestInput,
  type RejectSponsorshipRequestResult,
  type RejectSponsorshipRequestErrorCode,
} from './RejectSponsorshipRequestUseCase';
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';

describe('RejectSponsorshipRequestUseCase', () => {
  let useCase: RejectSponsorshipRequestUseCase;
  let sponsorshipRequestRepo: { findById: Mock; update: Mock };
  let logger: Logger;
  let output: UseCaseOutputPort<RejectSponsorshipRequestResult> & { present: Mock };

  beforeEach(() => {
    sponsorshipRequestRepo = { findById: vi.fn(), update: vi.fn() };
    logger = {
      debug: vi.fn(),
      info: vi.fn(),
      warn: vi.fn(),
      error: vi.fn(),
    };
    output = { present: vi.fn() } as unknown as UseCaseOutputPort<RejectSponsorshipRequestResult> & {
      present: Mock;
    };

    useCase = new RejectSponsorshipRequestUseCase(
      sponsorshipRequestRepo as unknown as ISponsorshipRequestRepository,
      logger,
      output,
    );
  });

  const unwrapError = (
    result: Result<void, ApplicationErrorCode<RejectSponsorshipRequestErrorCode, { message: string }>>,
  ): ApplicationErrorCode<RejectSponsorshipRequestErrorCode, { message: string }> => result.unwrapErr();

  it('should return not found error when request does not exist and not call output', async () => {
    sponsorshipRequestRepo.findById.mockResolvedValue(null);

    const input: RejectSponsorshipRequestInput = {
      requestId: 'request-1',
      respondedBy: 'driver-1',
      reason: 'Not interested',
    };

    const result = await useCase.execute(input);

    expect(result.isErr()).toBe(true);
    const error = unwrapError(result);
    expect(error.code).toBe('SPONSORSHIP_REQUEST_NOT_FOUND');
    expect(error.details?.message).toBe('Sponsorship request not found');
    expect(sponsorshipRequestRepo.update).not.toHaveBeenCalled();
    expect(output.present).not.toHaveBeenCalled();
  });

  it('should return not pending error when request is not pending and not call output', async () => {
    const mockRequest = {
      id: 'request-1',
      status: 'accepted',
      isPending: vi.fn().mockReturnValue(false),
    };
    sponsorshipRequestRepo.findById.mockResolvedValue(mockRequest);

    const input: RejectSponsorshipRequestInput = {
      requestId: 'request-1',
      respondedBy: 'driver-1',
      reason: 'Not interested',
    };

    const result = await useCase.execute(input);

    expect(result.isErr()).toBe(true);
    const error = unwrapError(result);
    expect(error.code).toBe('SPONSORSHIP_REQUEST_NOT_PENDING');
    expect(error.details?.message).toBe('Sponsorship request is not pending');
    expect(sponsorshipRequestRepo.update).not.toHaveBeenCalled();
    expect(output.present).not.toHaveBeenCalled();
  });

  it('should reject the request successfully with reason and present result once', async () => {
    const respondedAt = new Date('2023-01-01T00:00:00Z');
    const mockRequest = {
      id: 'request-1',
      status: 'pending',
      isPending: vi.fn().mockReturnValue(true),
      reject: vi.fn().mockReturnValue({
        id: 'request-1',
        respondedAt,
        rejectionReason: 'Not interested',
      }),
    };
    sponsorshipRequestRepo.findById.mockResolvedValue(mockRequest);
    sponsorshipRequestRepo.update.mockResolvedValue(undefined);

    const input: RejectSponsorshipRequestInput = {
      requestId: 'request-1',
      respondedBy: 'driver-1',
      reason: 'Not interested',
    };

    const result = await useCase.execute(input);

    expect(result.isOk()).toBe(true);
    expect(result.unwrap()).toBeUndefined();
    expect(sponsorshipRequestRepo.update).toHaveBeenCalledTimes(1);
    expect(mockRequest.reject).toHaveBeenCalledWith('driver-1', 'Not interested');
    expect(output.present).toHaveBeenCalledTimes(1);

    const [[presented]] = output.present.mock.calls as [[RejectSponsorshipRequestResult]];
    expect(presented.requestId).toBe('request-1');
    expect(presented.status).toBe('rejected');
    expect(presented.respondedAt).toBe(respondedAt);
    expect(presented.rejectionReason).toBe('Not interested');
  });

  it('should reject the request successfully without reason and present result once', async () => {
    const respondedAt = new Date('2023-01-01T00:00:00Z');
    const mockRequest = {
      id: 'request-1',
      status: 'pending',
      isPending: vi.fn().mockReturnValue(true),
      reject: vi.fn().mockReturnValue({
        id: 'request-1',
        respondedAt,
        rejectionReason: undefined,
      }),
    };
    sponsorshipRequestRepo.findById.mockResolvedValue(mockRequest);
    sponsorshipRequestRepo.update.mockResolvedValue(undefined);

    const input: RejectSponsorshipRequestInput = {
      requestId: 'request-1',
      respondedBy: 'driver-1',
    };

    const result = await useCase.execute(input);

    expect(result.isOk()).toBe(true);
    expect(result.unwrap()).toBeUndefined();
    expect(sponsorshipRequestRepo.update).toHaveBeenCalledTimes(1);
    expect(mockRequest.reject).toHaveBeenCalledWith('driver-1', undefined);
    expect(output.present).toHaveBeenCalledTimes(1);

    const [[presented]] = output.present.mock.calls as [[RejectSponsorshipRequestResult]];
    expect(presented.requestId).toBe('request-1');
    expect(presented.status).toBe('rejected');
    expect(presented.respondedAt).toBe(respondedAt);
    expect(presented.rejectionReason).toBeUndefined();
  });

  it('should wrap repository errors in REPOSITORY_ERROR and not call output', async () => {
    const error = new Error('DB failure');
    sponsorshipRequestRepo.findById.mockRejectedValue(error);

    const input: RejectSponsorshipRequestInput = {
      requestId: 'request-1',
      respondedBy: 'driver-1',
    };

    const result = await useCase.execute(input);

    expect(result.isErr()).toBe(true);
    const appError = unwrapError(result);
    expect(appError.code).toBe('REPOSITORY_ERROR');
    expect(appError.details?.message).toBe('DB failure');
    expect(output.present).not.toHaveBeenCalled();
  });
});
