import { requestContextMiddleware } from '@adapters/http/RequestContext';
import { Result } from '@core/shared/domain/Result';
import { describe, expect, it, vi } from 'vitest';
import { LeagueService } from './LeagueService';

async function withUserId<T>(userId: string, fn: () => Promise<T>): Promise<T> {
  const req = { user: { userId } };
  const res = {};

  return await new Promise<T>((resolve, reject) => {
    requestContextMiddleware(req as never, res as never, () => {
      fn().then(resolve, reject);
    });
  });
}

describe('LeagueService', () => {
  it('covers LeagueService happy paths and error branches', async () => {
    const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };

    const ok = async () => Result.ok(undefined);
    const err = async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } } as never);

    const getAllLeaguesWithCapacityUseCase = { execute: vi.fn(async () => Result.ok({ leagues: [] })) };
    const getAllLeaguesWithCapacityAndScoringUseCase = { execute: vi.fn(ok) };
    const getLeagueStandingsUseCase = { execute: vi.fn(ok) };
    const getLeagueStatsUseCase = { execute: vi.fn(ok) };
    const getLeagueFullConfigUseCase = { execute: vi.fn(ok) };
    const getLeagueScoringConfigUseCase = { execute: vi.fn(ok) };
    const listLeagueScoringPresetsUseCase = { execute: vi.fn(ok) };
    const joinLeagueUseCase = { execute: vi.fn(ok) };
    const transferLeagueOwnershipUseCase = { execute: vi.fn(ok) };
    const createLeagueWithSeasonAndScoringUseCase = { execute: vi.fn(ok) };
    const getTotalLeaguesUseCase = { execute: vi.fn(ok) };
    const getLeagueJoinRequestsUseCase = { execute: vi.fn(ok) };
    const approveLeagueJoinRequestUseCase = { execute: vi.fn(ok) };
    const rejectLeagueJoinRequestUseCase = { execute: vi.fn(ok) };
    const removeLeagueMemberUseCase = { execute: vi.fn(ok) };
    const updateLeagueMemberRoleUseCase = { execute: vi.fn(ok) };
    const getLeagueOwnerSummaryUseCase = { execute: vi.fn(ok) };
    const getLeagueProtestsUseCase = { execute: vi.fn(ok) };
    const getLeagueSeasonsUseCase = { execute: vi.fn(ok) };
    const getLeagueMembershipsUseCase = { execute: vi.fn(ok) };
    const getLeagueScheduleUseCase = { execute: vi.fn(ok) };
    const getLeagueAdminPermissionsUseCase = { execute: vi.fn(ok) };
    const getLeagueWalletUseCase = { execute: vi.fn(ok) };
    const withdrawFromLeagueWalletUseCase = { execute: vi.fn(ok) };
    const getSeasonSponsorshipsUseCase = { execute: vi.fn(ok) };

    const getLeagueRosterMembersUseCase = { execute: vi.fn(ok) };
    const getLeagueRosterJoinRequestsUseCase = { execute: vi.fn(ok) };

    // Schedule mutation use cases (must be called by LeagueService, not repositories)
    const createLeagueSeasonScheduleRaceUseCase = { execute: vi.fn(ok) };
    const updateLeagueSeasonScheduleRaceUseCase = { execute: vi.fn(ok) };
    const deleteLeagueSeasonScheduleRaceUseCase = { execute: vi.fn(ok) };
    const publishLeagueSeasonScheduleUseCase = { execute: vi.fn(ok) };
    const unpublishLeagueSeasonScheduleUseCase = { execute: vi.fn(ok) };

    const allLeaguesWithCapacityPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ leagues: [] })) };
    const allLeaguesWithCapacityAndScoringPresenter = {
      present: vi.fn(),
      getViewModel: vi.fn(() => ({ leagues: [], totalCount: 0 })),
    };
    const leagueStandingsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ standings: [] })) };
    const leagueProtestsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ protests: [] })) };
    const seasonSponsorshipsPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ sponsorships: [] })) };
    const leagueScoringPresetsPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ presets: [] })) };
    const approveLeagueJoinRequestPresenter = {
      present: vi.fn(),
      getViewModel: vi.fn(() => ({ success: true }))
    };
    const createLeaguePresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ id: 'l1' })) };
    const getLeagueAdminPermissionsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ canManage: true })) };
    const getLeagueMembershipsPresenter = {
      reset: vi.fn(),
      present: vi.fn(),
      getViewModel: vi.fn(() => ({ memberships: { memberships: [] } })),
    };

    const getLeagueRosterMembersPresenter = {
      reset: vi.fn(),
      present: vi.fn(),
      getViewModel: vi.fn(() => ([])),
    };

    const getLeagueRosterJoinRequestsPresenter = {
      reset: vi.fn(),
      present: vi.fn(),
      getViewModel: vi.fn(() => ([])),
    };
    const getLeagueOwnerSummaryPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ ownerId: 'o1' })) };
    const getLeagueSeasonsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ([])) };
    const joinLeaguePresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ success: true })) };
    const leagueSchedulePresenter = { reset: vi.fn(), present: vi.fn(), getViewModel: vi.fn(() => ({ seasonId: 'season-1', published: false, races: [] })) };
    const leagueStatsPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ stats: {} })) };
    const rejectLeagueJoinRequestPresenter = {
      present: vi.fn(),
      getViewModel: vi.fn(() => ({ success: true }))
    };
    const removeLeagueMemberPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ success: true })) };
    const totalLeaguesPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ total: 1 })) };
    const transferLeagueOwnershipPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ success: true })) };
    const updateLeagueMemberRolePresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ success: true })) };
    const leagueConfigPresenter = { 
      present: vi.fn(),
      getViewModel: vi.fn(() => ({ form: {} })) 
    };
    const leagueScoringConfigPresenter = { 
      present: vi.fn(),
      getViewModel: vi.fn(() => ({ config: {} })) 
    };
    const getLeagueWalletPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ balance: 0 })) };
    const withdrawFromLeagueWalletPresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true })) };
    const leagueJoinRequestsPresenter = { reset: vi.fn(), present: vi.fn(), getViewModel: vi.fn(() => ({ joinRequests: [] })) };

    const createLeagueSeasonScheduleRacePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ raceId: 'race-1' })) };
    const updateLeagueSeasonScheduleRacePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true })) };
    const deleteLeagueSeasonScheduleRacePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true })) };
    const publishLeagueSeasonSchedulePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true, published: true })) };
    const unpublishLeagueSeasonSchedulePresenter = { present: vi.fn(), getResponseModel: vi.fn(() => ({ success: true, published: false })) };

    const service = new (LeagueService as unknown as { new (...args: never[]): LeagueService })(
      getAllLeaguesWithCapacityUseCase as never,
      getAllLeaguesWithCapacityAndScoringUseCase as never,
      getLeagueStandingsUseCase as never,
      getLeagueStatsUseCase as never,
      getLeagueFullConfigUseCase as never,
      getLeagueScoringConfigUseCase as never,
      listLeagueScoringPresetsUseCase as never,
      joinLeagueUseCase as never,
      transferLeagueOwnershipUseCase as never,
      createLeagueWithSeasonAndScoringUseCase as never,
      getTotalLeaguesUseCase as never,
      getLeagueJoinRequestsUseCase as never,
      approveLeagueJoinRequestUseCase as never,
      rejectLeagueJoinRequestUseCase as never,
      removeLeagueMemberUseCase as never,
      updateLeagueMemberRoleUseCase as never,
      getLeagueOwnerSummaryUseCase as never,
      getLeagueProtestsUseCase as never,
      getLeagueSeasonsUseCase as never,
      getLeagueMembershipsUseCase as never,
      getLeagueScheduleUseCase as never,
      getLeagueAdminPermissionsUseCase as never,
      getLeagueWalletUseCase as never,
      withdrawFromLeagueWalletUseCase as never,
      getSeasonSponsorshipsUseCase as never,
      createLeagueSeasonScheduleRaceUseCase as never,
      updateLeagueSeasonScheduleRaceUseCase as never,
      deleteLeagueSeasonScheduleRaceUseCase as never,
      publishLeagueSeasonScheduleUseCase as never,
      unpublishLeagueSeasonScheduleUseCase as never,
      logger as never,
      allLeaguesWithCapacityPresenter as never,
      allLeaguesWithCapacityAndScoringPresenter as never,
      leagueStandingsPresenter as never,
      leagueProtestsPresenter as never,
      seasonSponsorshipsPresenter as never,
      leagueScoringPresetsPresenter as never,
      approveLeagueJoinRequestPresenter as never,
      createLeaguePresenter as never,
      getLeagueAdminPermissionsPresenter as never,
      getLeagueMembershipsPresenter as never,
      getLeagueOwnerSummaryPresenter as never,
      getLeagueSeasonsPresenter as never,
      joinLeaguePresenter as never,
      leagueSchedulePresenter as never,
      leagueStatsPresenter as never,
      rejectLeagueJoinRequestPresenter as never,
      removeLeagueMemberPresenter as never,
      totalLeaguesPresenter as never,
      transferLeagueOwnershipPresenter as never,
      updateLeagueMemberRolePresenter as never,
      leagueConfigPresenter as never,
      leagueScoringConfigPresenter as never,
      getLeagueWalletPresenter as never,
      withdrawFromLeagueWalletPresenter as never,
      leagueJoinRequestsPresenter as never,
      createLeagueSeasonScheduleRacePresenter as never,
      updateLeagueSeasonScheduleRacePresenter as never,
      deleteLeagueSeasonScheduleRacePresenter as never,
      publishLeagueSeasonSchedulePresenter as never,
      unpublishLeagueSeasonSchedulePresenter as never,

      // Roster admin read delegation (added for strict TDD)
      getLeagueRosterMembersUseCase as never,
      getLeagueRosterJoinRequestsUseCase as never,
      getLeagueRosterMembersPresenter as never,
      getLeagueRosterJoinRequestsPresenter as never,
    );

    await expect(service.getTotalLeagues()).resolves.toEqual({ total: 1 });
    await withUserId('user-1', async () => {
      await expect(service.getLeagueJoinRequests('l1')).resolves.toEqual([]);
    });

    await withUserId('user-1', async () => {
      await expect(service.approveLeagueJoinRequest({ leagueId: 'l1', requestId: 'r1' } as never)).resolves.toEqual({
        success: true,
      });
    });

    await withUserId('user-1', async () => {
      await expect(service.rejectLeagueJoinRequest({ leagueId: 'l1', requestId: 'r1' } as never)).resolves.toEqual({
        success: true,
      });
    });

    expect(rejectLeagueJoinRequestUseCase.execute).toHaveBeenCalledWith(
      { leagueId: 'l1', joinRequestId: 'r1' }
    );

    await withUserId('user-1', async () => {
      await expect(service.getLeagueAdminPermissions({ leagueId: 'l1' } as never)).resolves.toEqual({ canManage: true });
    });

    await withUserId('user-1', async () => {
      await expect(service.removeLeagueMember({ leagueId: 'l1', targetDriverId: 'd1' } as never)).resolves.toEqual({
        success: true,
      });
    });

    expect(getLeagueAdminPermissionsUseCase.execute).toHaveBeenCalledWith({
      leagueId: 'l1',
      performerDriverId: 'user-1',
    });

    expect(removeLeagueMemberUseCase.execute).toHaveBeenCalledWith({
      leagueId: 'l1',
      targetDriverId: 'd1',
    });

    await withUserId('user-1', async () => {
      await expect(service.updateLeagueMemberRole('l1', 'd1', { newRole: 'member' } as never)).resolves.toEqual({
        success: true,
      });
    });

    expect(getLeagueAdminPermissionsUseCase.execute).toHaveBeenCalledWith({
      leagueId: 'l1',
      performerDriverId: 'user-1',
    });

    expect(updateLeagueMemberRoleUseCase.execute).toHaveBeenCalledWith({
      leagueId: 'l1',
      targetDriverId: 'd1',
      newRole: 'member',
    });

    await expect(service.getLeagueOwnerSummary({ leagueId: 'l1' } as never)).resolves.toEqual({ ownerId: 'o1' });
    await expect(service.getLeagueProtests({ leagueId: 'l1' } as never)).resolves.toEqual({ protests: [] });
    await expect(service.getLeagueSeasons({ leagueId: 'l1' } as never)).resolves.toEqual([]);

    await expect(service.getLeagueFullConfig({ leagueId: 'l1' } as never)).resolves.toEqual({ form: {} });
    await expect(service.getLeagueScoringConfig('l1')).resolves.toEqual({ config: {} });

    await expect(service.getLeagueMemberships('l1')).resolves.toEqual({ memberships: [] });

    // Roster admin read endpoints must delegate to core use cases (tests fail until refactor)
    await withUserId('user-1', async () => {
      await service.getLeagueRosterMembers('l1');
    });
    expect(getLeagueRosterMembersUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1' });
    expect(getLeagueRosterMembersPresenter.reset).toHaveBeenCalled();

    await withUserId('user-1', async () => {
      await service.getLeagueRosterJoinRequests('l1');
    });
    expect(getLeagueRosterJoinRequestsUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1' });
    expect(getLeagueRosterJoinRequestsPresenter.reset).toHaveBeenCalled();

    // Roster admin read endpoints must be admin-gated (auth boundary) and must not execute core use cases on 403.
    getLeagueAdminPermissionsUseCase.execute.mockResolvedValueOnce(
      Result.err({ code: 'FORBIDDEN', details: { message: 'nope' } }) as never,
    );
    getLeagueRosterMembersUseCase.execute.mockClear();
    await withUserId('user-2', async () => {
      await expect(service.getLeagueRosterMembers('l1')).rejects.toThrow('Forbidden');
    });
    expect(getLeagueRosterMembersUseCase.execute).not.toHaveBeenCalled();

    await expect(service.getLeagueStandings('l1')).resolves.toEqual({ standings: [] });
    await expect(service.getLeagueSchedule('l1')).resolves.toEqual({ seasonId: 'season-1', published: false, races: [] });
    expect(getLeagueScheduleUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1' });

    getLeagueScheduleUseCase.execute.mockClear();
    await expect(service.getLeagueSchedule('l1', { seasonId: 'season-x' } as never)).resolves.toEqual({
      seasonId: 'season-1',
      published: false,
      races: [],
    });
    expect(getLeagueScheduleUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1', seasonId: 'season-x' });
    await expect(service.getLeagueStats('l1')).resolves.toEqual({ stats: {} });

    await expect(service.createLeague({ name: 'n', description: 'd', ownerId: 'o' } as never)).resolves.toEqual({ id: 'l1' });
    await expect(service.listLeagueScoringPresets()).resolves.toEqual({ presets: [] });

    await withUserId('user-1', async () => {
      await expect(service.joinLeague('l1')).resolves.toEqual({ success: true });
    });

    await withUserId('user-1', async () => {
      await expect(service.transferLeagueOwnership('l1', { newOwnerId: 'o2' } as never)).resolves.toEqual({ success: true });
    });

    // Transfer ownership must be admin-gated and actor-derived (no payload owner/admin IDs)
    getLeagueAdminPermissionsUseCase.execute.mockClear();
    transferLeagueOwnershipUseCase.execute.mockClear();

    await withUserId('user-1', async () => {
      await expect(
        service.transferLeagueOwnership('l1', { newOwnerId: 'o2', currentOwnerId: 'spoof' } as never),
      ).resolves.toEqual({ success: true });
    });

    expect(getLeagueAdminPermissionsUseCase.execute).toHaveBeenCalledWith({
      leagueId: 'l1',
      performerDriverId: 'user-1',
    });

    expect(transferLeagueOwnershipUseCase.execute).toHaveBeenCalledWith({
      leagueId: 'l1',
      currentOwnerId: 'user-1',
      newOwnerId: 'o2',
    });

    // Unauthorized (non-admin/owner) actors are rejected
    getLeagueAdminPermissionsUseCase.execute.mockResolvedValueOnce(
      Result.err({ code: 'FORBIDDEN', details: { message: 'nope' } }) as never,
    );
    transferLeagueOwnershipUseCase.execute.mockClear();
    await withUserId('user-2', async () => {
      await expect(service.transferLeagueOwnership('l1', { newOwnerId: 'o2' } as never)).rejects.toThrow('Forbidden');
    });
    expect(transferLeagueOwnershipUseCase.execute).not.toHaveBeenCalled();

    // Actor must be derived from session (request context), not payload arguments.
    joinLeagueUseCase.execute.mockClear();
    await withUserId('user-1', async () => {
      await expect(service.joinLeague('l1')).resolves.toEqual({ success: true });
    });
    expect(joinLeagueUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1', driverId: 'user-1' });

    await expect(service.getSeasonSponsorships('s1')).resolves.toEqual({ sponsorships: [] });
    await expect(service.getRaces('l1')).resolves.toEqual({ races: [] });

    await expect(service.getLeagueWallet('l1')).resolves.toEqual({ balance: 0 });

    await withUserId('user-1', async () => {
      await expect(
        service.publishLeagueSeasonSchedule('l1', 'season-1', {} as never),
      ).resolves.toEqual({ success: true, published: true });

      await expect(
        service.unpublishLeagueSeasonSchedule('l1', 'season-1', {} as never),
      ).resolves.toEqual({ success: true, published: false });

      await expect(
        service.createLeagueSeasonScheduleRace('l1', 'season-1', {
          track: 'Spa',
          car: 'GT3',
          scheduledAtIso: new Date('2025-01-10T20:00:00Z').toISOString(),
        } as never),
      ).resolves.toEqual({ raceId: 'race-1' });

      await expect(
        service.updateLeagueSeasonScheduleRace('l1', 'season-1', 'race-1', {
          track: 'Monza',
          car: 'LMP2',
        } as never),
      ).resolves.toEqual({ success: true });

      await expect(
        service.deleteLeagueSeasonScheduleRace('l1', 'season-1', 'race-1'),
      ).resolves.toEqual({ success: true });
    });

    expect(publishLeagueSeasonScheduleUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1', seasonId: 'season-1' });
    expect(unpublishLeagueSeasonScheduleUseCase.execute).toHaveBeenCalledWith({ leagueId: 'l1', seasonId: 'season-1' });

    expect(createLeagueSeasonScheduleRaceUseCase.execute).toHaveBeenCalledWith({
      leagueId: 'l1',
      seasonId: 'season-1',
      track: 'Spa',
      car: 'GT3',
      scheduledAt: expect.any(Date),
    });

    expect(updateLeagueSeasonScheduleRaceUseCase.execute).toHaveBeenCalledWith({
      leagueId: 'l1',
      seasonId: 'season-1',
      raceId: 'race-1',
      track: 'Monza',
      car: 'LMP2',
    });

    expect(deleteLeagueSeasonScheduleRaceUseCase.execute).toHaveBeenCalledWith({
      leagueId: 'l1',
      seasonId: 'season-1',
      raceId: 'race-1',
    });

    await withUserId('user-1', async () => {
      await expect(
        service.withdrawFromLeagueWallet('l1', { amount: 1, currency: 'USD', destinationAccount: 'x' } as never),
      ).resolves.toEqual({
        success: true,
      });
    });

    expect(withdrawFromLeagueWalletUseCase.execute).toHaveBeenCalledWith({
      leagueId: 'l1',
      requestedById: 'user-1',
      amount: 1,
      currency: 'USD',
      reason: 'x',
    });

    await expect(service.getAllLeaguesWithCapacity()).resolves.toEqual({ leagues: [] });
    await expect(service.getAllLeaguesWithCapacityAndScoring()).resolves.toEqual({ leagues: [], totalCount: 0 });

    // Error branch: getAllLeaguesWithCapacity throws on result.isErr()
    getAllLeaguesWithCapacityUseCase.execute.mockResolvedValueOnce(Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } } as never));
    await expect(service.getAllLeaguesWithCapacity()).rejects.toThrow('REPOSITORY_ERROR');

    // Error branches: use case returns error result
    getLeagueFullConfigUseCase.execute.mockResolvedValueOnce(
      Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } } as never)
    );
    await expect(service.getLeagueFullConfig({ leagueId: 'l1' } as never)).resolves.toBeNull();

    getLeagueScoringConfigUseCase.execute.mockImplementationOnce(async () => {
      throw new Error('boom');
    });
    await expect(service.getLeagueScoringConfig('l1')).resolves.toBeNull();

    // Cover non-Error throw branches for logger.error wrapping
    getLeagueFullConfigUseCase.execute.mockResolvedValueOnce(
      Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } } as never)
    );
    await expect(service.getLeagueFullConfig({ leagueId: 'l1' } as never)).resolves.toBeNull();

    getLeagueScoringConfigUseCase.execute.mockImplementationOnce(async () => {
      throw 'boom';
    });
    await expect(service.getLeagueScoringConfig('l1')).resolves.toBeNull();

    // getLeagueAdmin error branch: fullConfigResult is Err
    getLeagueFullConfigUseCase.execute.mockResolvedValueOnce(Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } } as never));
    await withUserId('user-1', async () => {
      await expect(service.getLeagueAdmin('l1')).rejects.toThrow('REPOSITORY_ERROR');
    });

    // getLeagueAdmin happy path
    getLeagueFullConfigUseCase.execute.mockResolvedValueOnce(Result.ok(undefined));
    await withUserId('user-1', async () => {
      await expect(service.getLeagueAdmin('l1')).resolves.toEqual({
        joinRequests: [],
        ownerSummary: { ownerId: 'o1' },
        config: { form: { form: {} } },
        protests: { protests: [] },
        seasons: [],
      });
    });

    // keep lint happy (ensures err() used)
    await err();
  });
});
