import { describe, expect, it, vi } from 'vitest';
import { Result } from '@core/shared/application/Result';
import { requestContextMiddleware } from '@adapters/http/RequestContext';
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 any, res as any, () => {
      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' } });

    const getAllLeaguesWithCapacityUseCase: any = { execute: vi.fn(async () => Result.ok({ leagues: [] })) };
    const getAllLeaguesWithCapacityAndScoringUseCase: any = { execute: vi.fn(ok) };
    const getLeagueStandingsUseCase = { execute: vi.fn(ok) };
    const getLeagueStatsUseCase = { execute: vi.fn(ok) };
    const getLeagueFullConfigUseCase: any = { 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 = { getResponseModel: vi.fn(() => ({ standings: [] })) };
    const leagueProtestsPresenter = { getResponseModel: vi.fn(() => ({ protests: [] })) };
    const seasonSponsorshipsPresenter = { getViewModel: vi.fn(() => ({ sponsorships: [] })) };
    const leagueScoringPresetsPresenter = { getViewModel: vi.fn(() => ({ presets: [] })) };
    const approveLeagueJoinRequestPresenter = { getViewModel: vi.fn(() => ({ success: true })) };
    const createLeaguePresenter = { getViewModel: vi.fn(() => ({ id: 'l1' })) };
    const getLeagueAdminPermissionsPresenter = { getResponseModel: vi.fn(() => ({ canManage: true })) };
    const getLeagueMembershipsPresenter = {
      reset: 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 = { getViewModel: vi.fn(() => ({ ownerId: 'o1' })) };
    const getLeagueSeasonsPresenter = { getResponseModel: vi.fn(() => ([])) };
    const joinLeaguePresenter = { getViewModel: vi.fn(() => ({ success: true })) };
    const leagueSchedulePresenter = { getViewModel: vi.fn(() => ({ seasonId: 'season-1', published: false, races: [] })) };
    const leagueStatsPresenter = { getResponseModel: vi.fn(() => ({ stats: {} })) };
    const rejectLeagueJoinRequestPresenter = { getViewModel: vi.fn(() => ({ success: true })) };
    const removeLeagueMemberPresenter = { getViewModel: vi.fn(() => ({ success: true })) };
    const totalLeaguesPresenter = { getResponseModel: vi.fn(() => ({ total: 1 })) };
    const transferLeagueOwnershipPresenter = { getViewModel: vi.fn(() => ({ success: true })) };
    const updateLeagueMemberRolePresenter = { getViewModel: vi.fn(() => ({ success: true })) };
    const leagueConfigPresenter = { getViewModel: vi.fn(() => ({ form: {} })) };
    const leagueScoringConfigPresenter = { getViewModel: vi.fn(() => ({ config: {} })) };
    const getLeagueWalletPresenter = { getResponseModel: vi.fn(() => ({ balance: 0 })) };
    const withdrawFromLeagueWalletPresenter = { getResponseModel: vi.fn(() => ({ success: true })) };
    const leagueJoinRequestsPresenter = { getViewModel: vi.fn(() => ({ joinRequests: [] })) };

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

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

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

    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 any)).resolves.toEqual({
        success: true,
      });
    });

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

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

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

    await withUserId('user-1', async () => {
      await expect(service.removeLeagueMember({ leagueId: 'l1', targetDriverId: 'd1' } as any)).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 any)).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 any)).resolves.toEqual({ ownerId: 'o1' });
    await expect(service.getLeagueProtests({ leagueId: 'l1' } as any)).resolves.toEqual({ protests: [] });
    await expect(service.getLeagueSeasons({ leagueId: 'l1' } as any)).resolves.toEqual([]);

    await expect(service.getLeagueFullConfig({ leagueId: 'l1' } as any)).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 any,
    );
    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 any)).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 any)).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 any)).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 any),
      ).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 any,
    );
    transferLeagueOwnershipUseCase.execute.mockClear();
    await withUserId('user-2', async () => {
      await expect(service.transferLeagueOwnership('l1', { newOwnerId: 'o2' } as any)).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 any),
      ).resolves.toEqual({ success: true, published: true });

      await expect(
        service.unpublishLeagueSeasonSchedule('l1', 'season-1', {} as any),
      ).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 any),
      ).resolves.toEqual({ raceId: 'race-1' });

      await expect(
        service.updateLeagueSeasonScheduleRace('l1', 'season-1', 'race-1', {
          track: 'Monza',
          car: 'LMP2',
        } as any),
      ).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 any),
      ).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' } }));
    await expect(service.getAllLeaguesWithCapacity()).rejects.toThrow('REPOSITORY_ERROR');

    // Error branches: try/catch returning null
    getLeagueFullConfigUseCase.execute.mockImplementationOnce(async () => {
      throw new Error('boom');
    });
    await expect(service.getLeagueFullConfig({ leagueId: 'l1' } as any)).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.mockImplementationOnce(async () => {
      throw 'boom';
    });
    await expect(service.getLeagueFullConfig({ leagueId: 'l1' } as any)).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' } }));
    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();
  });
});
