import { describe, expect, it, vi } from 'vitest';

import {
    CreateSeasonForLeagueUseCase,
    GetSeasonDetailsUseCase,
    ListSeasonsForLeagueUseCase,
    ManageSeasonLifecycleUseCase,
    type CreateSeasonForLeagueCommand,
    type CreateSeasonForLeagueErrorCode,
    type GetSeasonDetailsErrorCode,
    type LeagueConfigFormModel,
    type ListSeasonsForLeagueErrorCode,
    type ManageSeasonLifecycleCommand,
    type ManageSeasonLifecycleErrorCode,
} from '@core/racing/application/use-cases/SeasonUseCases';
import { League } from '@core/racing/domain/entities/League';
import { Season } from '@core/racing/domain/entities/season/Season';
import type { LeagueRepository } from '@core/racing/domain/repositories/LeagueRepository';
import type { SeasonRepository } from '@core/racing/domain/repositories/SeasonRepository';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';

function getUnknownString(value: unknown): string | null {
  if (typeof value === 'string') return value;
  if (
    value &&
    typeof value === 'object' &&
    'toString' in value &&
    typeof (value as { toString: unknown }).toString === 'function'
  ) {
    return (value as { toString: () => string }).toString();
  }
  return null;
}

function createFakeLeagueRepository(seed: Array<{ id: string }>): LeagueRepository {
  const leagues: League[] = seed.map(({ id }) =>
    League.create({
      id,
      name: `League ${id}`,
      description: 'Test league',
      ownerId: 'owner-1',
    }),
  );

  return {
    findById: async (id: string) =>
      leagues.find((league) => league.id.toString() === id) ?? null,
    findAll: async () => leagues,
    findByOwnerId: async (ownerId: string) =>
      leagues.filter((league) => getUnknownString((league as unknown as { ownerId: unknown }).ownerId) === ownerId),
    create: async (league: League) => league,
    update: async (league: League) => league,
    delete: async () => undefined,
    exists: async (id: string) => leagues.some((league) => league.id.toString() === id),
    searchByName: async () => [],
  };
}

function createLeagueConfigFormModel(overrides?: Partial<LeagueConfigFormModel>): LeagueConfigFormModel {
  return {
    basics: {
      name: 'Test League',
      visibility: 'ranked',
      gameId: 'iracing',
      ...overrides?.basics,
    },
    structure: {
      mode: 'solo',
      maxDrivers: 30,
      ...overrides?.structure,
    },
    championships: {
      enableDriverChampionship: true,
      enableTeamChampionship: false,
      enableNationsChampionship: false,
      enableTrophyChampionship: false,
      ...overrides?.championships,
    },
    scoring: {
      patternId: 'sprint-main-driver',
      customScoringEnabled: false,
      ...overrides?.scoring,
    },
    dropPolicy: {
      strategy: 'bestNResults',
      n: 3,
      ...overrides?.dropPolicy,
    },
    timings: {
      qualifyingMinutes: 10,
      mainRaceMinutes: 30,
      sessionCount: 8,
      seasonStartDate: '2025-01-01',
      raceStartTime: '20:00',
      timezoneId: 'UTC',
      recurrenceStrategy: 'weekly',
      weekdays: ['Mon'],
      ...overrides?.timings,
    },
    stewarding: {
      decisionMode: 'steward_vote',
      requiredVotes: 3,
      requireDefense: true,
      defenseTimeLimit: 24,
      voteTimeLimit: 24,
      protestDeadlineHours: 48,
      stewardingClosesHours: 72,
      notifyAccusedOnProtest: true,
      notifyOnVoteRequired: true,
      ...overrides?.stewarding,
    },
    ...overrides,
  };
}

describe('CreateSeasonForLeagueUseCase', () => {
  function setup() {
    const leagueRepo = createFakeLeagueRepository([{ id: 'league-1' }]);
    const seasonRepo: SeasonRepository = {
      add: vi.fn(),
      findById: vi.fn(),
      update: vi.fn(),
      listByLeague: vi.fn(),
      listActiveByLeague: vi.fn(),
    } as unknown as SeasonRepository;

    const useCase = new CreateSeasonForLeagueUseCase(leagueRepo, seasonRepo);

    return { leagueRepo, seasonRepo, useCase };
  }

  it('creates a planned Season for an existing league with config-derived props', async () => {
    const { seasonRepo, useCase } = setup();

    const config = createLeagueConfigFormModel({
      basics: {
        name: 'League With Config',
        visibility: 'ranked',
        gameId: 'iracing',
      },
      scoring: {
        patternId: 'club-default',
        customScoringEnabled: true,
      },
      dropPolicy: {
        strategy: 'dropWorstN',
        n: 2,
      },
      timings: {
        qualifyingMinutes: 10,
        mainRaceMinutes: 30,
        sessionCount: 8,
      },
    });

    (seasonRepo.add as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);

    const command: CreateSeasonForLeagueCommand = {
      leagueId: 'league-1',
      name: 'Season from Config',
      gameId: 'iracing',
      config,
    };

    const result = await useCase.execute(command);

    expect(result.isOk()).toBe(true);
    const payload = result.unwrap();
    expect(payload.season).toBeDefined();
    
    const season = payload.season;
    expect(season.leagueId).toBe('league-1');
    expect(season.gameId).toBe('iracing');
    expect(season.name).toBe('Season from Config');
    expect(season.status.toString()).toBe('planned');

    // Schedule is optional when timings lack seasonStartDate / raceStartTime.
    expect(season.schedule).toBeUndefined();
    expect(season.scoringConfig).toBeDefined();
    expect(season.scoringConfig!.scoringPresetId).toBe('club-default');
    expect(season.scoringConfig!.customScoringEnabled).toBe(true);

    expect(season.dropPolicy).toBeDefined();
    expect(season.dropPolicy!.strategy).toBe('dropWorstN');
    expect(season.dropPolicy!.n).toBe(2);

    expect(season.stewardingConfig).toBeDefined();
    expect(season.maxDrivers).toBe(30);
  });

  it('clones configuration from a source season when sourceSeasonId is provided', async () => {
    const { seasonRepo, useCase } = setup();

    const sourceSeason = Season.create({
      id: 'source-season',
      leagueId: 'league-1',
      gameId: 'iracing',
      name: 'Source Season',
      status: 'planned',
    }).withMaxDrivers(40);

    (seasonRepo.findById as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce(sourceSeason);
    (seasonRepo.add as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);

    const command: CreateSeasonForLeagueCommand = {
      leagueId: 'league-1',
      name: 'Cloned Season',
      gameId: 'iracing',
      sourceSeasonId: 'source-season',
    };

    const result = await useCase.execute(command);

    expect(result.isOk()).toBe(true);
    const payload = result.unwrap();
    expect(payload.season).toBeDefined();

    const season = payload.season;
    expect(season.id).not.toBe(sourceSeason.id);
    expect(season.leagueId).toBe(sourceSeason.leagueId);
    expect(season.gameId).toBe(sourceSeason.gameId);
    expect(season.status.toString()).toBe('planned');
    expect(season.maxDrivers).toBe(sourceSeason.maxDrivers);
    expect(season.schedule).toBe(sourceSeason.schedule);
    expect(season.scoringConfig).toBe(sourceSeason.scoringConfig);
    expect(season.dropPolicy).toBe(sourceSeason.dropPolicy);
    expect(season.stewardingConfig).toBe(sourceSeason.stewardingConfig);
  });

  it('returns LEAGUE_NOT_FOUND error when league does not exist', async () => {
    const leagueRepo = createFakeLeagueRepository([]);
    const seasonRepo: SeasonRepository = {
      add: vi.fn(),
      findById: vi.fn(),
      update: vi.fn(),
      listByLeague: vi.fn(),
      listActiveByLeague: vi.fn(),
    } as unknown as SeasonRepository;

    const useCase = new CreateSeasonForLeagueUseCase(leagueRepo, seasonRepo);

    const command: CreateSeasonForLeagueCommand = {
      leagueId: 'missing-league',
      name: 'Season',
      gameId: 'iracing',
    };

    const result = await useCase.execute(command);

    expect(result.isErr()).toBe(true);
    const error = result.unwrapErr() as ApplicationErrorCode<CreateSeasonForLeagueErrorCode, { message: string }>;
    expect(error.code).toBe('LEAGUE_NOT_FOUND');
    expect(error.details?.message).toContain('League not found');
  });
});

describe('ListSeasonsForLeagueUseCase', () => {
  function setup() {
    const leagueRepo = createFakeLeagueRepository([{ id: 'league-1' }]);
    const seasonRepo: SeasonRepository = {
      add: vi.fn(),
      findById: vi.fn(),
      update: vi.fn(),
      listByLeague: vi.fn(),
      listActiveByLeague: vi.fn(),
    } as unknown as SeasonRepository;

    const useCase = new ListSeasonsForLeagueUseCase(leagueRepo, seasonRepo);

    return { leagueRepo, seasonRepo, useCase };
  }

  it('lists seasons for a league with summaries', async () => {
    const { seasonRepo, useCase } = setup();

    const s1 = Season.create({
      id: 'season-1',
      leagueId: 'league-1',
      gameId: 'iracing',
      name: 'Season One',
      status: 'planned',
    });
    const s2 = Season.create({
      id: 'season-2',
      leagueId: 'league-1',
      gameId: 'iracing',
      name: 'Season Two',
      status: 'active',
    });
    const sOtherLeague = Season.create({
      id: 'season-3',
      leagueId: 'league-2',
      gameId: 'iracing',
      name: 'Season Other',
      status: 'planned',
    });

    (seasonRepo.listByLeague as unknown as ReturnType<typeof vi.fn>).mockResolvedValue([
      s1,
      s2,
      sOtherLeague,
    ]);

    const result = await useCase.execute({ leagueId: 'league-1' });

    expect(result.isOk()).toBe(true);
    const payload = result.unwrap();
    expect(payload.seasons).toBeDefined();

    const league1Seasons = payload.seasons.filter((s) => s.leagueId === 'league-1');
    expect(league1Seasons.map((s) => s.id).sort()).toEqual(['season-1', 'season-2']);
  });

  it('returns LEAGUE_NOT_FOUND error when league does not exist', async () => {
    const leagueRepo = createFakeLeagueRepository([]);
    const seasonRepo: SeasonRepository = {
      add: vi.fn(),
      findById: vi.fn(),
      update: vi.fn(),
      listByLeague: vi.fn(),
      listActiveByLeague: vi.fn(),
    } as unknown as SeasonRepository;

    const useCase = new ListSeasonsForLeagueUseCase(leagueRepo, seasonRepo);

    const result = await useCase.execute({ leagueId: 'missing-league' });

    expect(result.isErr()).toBe(true);
    const error = result.unwrapErr() as ApplicationErrorCode<ListSeasonsForLeagueErrorCode, { message: string }>;
    expect(error.code).toBe('LEAGUE_NOT_FOUND');
    expect(error.details?.message).toContain('League not found');
  });
});

describe('GetSeasonDetailsUseCase', () => {
  function setup(leagueSeed: Array<{ id: string }>) {
    const leagueRepo = createFakeLeagueRepository(leagueSeed);
    const seasonRepo: SeasonRepository = {
      add: vi.fn(),
      findById: vi.fn(),
      update: vi.fn(),
      listByLeague: vi.fn(),
      listActiveByLeague: vi.fn(),
    } as unknown as SeasonRepository;

    const useCase = new GetSeasonDetailsUseCase(leagueRepo, seasonRepo);

    return { leagueRepo, seasonRepo, useCase };
  }

  it('returns full details for a season belonging to the league', async () => {
    const { seasonRepo, useCase } = setup([{ id: 'league-1' }]);

    const season = Season.create({
      id: 'season-1',
      leagueId: 'league-1',
      gameId: 'iracing',
      name: 'Detailed Season',
      status: 'planned',
    }).withMaxDrivers(24);

    (seasonRepo.findById as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(season);

    const result = await useCase.execute({
      leagueId: 'league-1',
      seasonId: 'season-1',
    });

    expect(result.isOk()).toBe(true);
    const payload = result.unwrap();
    expect(payload.season).toBeDefined();

    expect(payload.season.id).toBe('season-1');
    expect(payload.season.leagueId).toBe('league-1');
    expect(payload.season.gameId).toBe('iracing');
    expect(payload.season.name).toBe('Detailed Season');
    expect(payload.season.status.toString()).toBe('planned');
    expect(payload.season.maxDrivers).toBe(24);
  });

  it('returns LEAGUE_NOT_FOUND when league does not exist', async () => {
    const { useCase } = setup([]);

    const result = await useCase.execute({
      leagueId: 'missing-league',
      seasonId: 'season-1',
    });

    expect(result.isErr()).toBe(true);
    const error = result.unwrapErr() as ApplicationErrorCode<GetSeasonDetailsErrorCode, { message: string }>;
    expect(error.code).toBe('LEAGUE_NOT_FOUND');
    expect(error.details?.message).toContain('League not found');
  });

  it('returns SEASON_NOT_FOUND when season does not belong to league', async () => {
    const { seasonRepo, useCase } = setup([{ id: 'league-1' }]);

    const season = Season.create({
      id: 'season-1',
      leagueId: 'other-league',
      gameId: 'iracing',
      name: 'Detailed Season',
      status: 'planned',
    });

    (seasonRepo.findById as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(season);

    const result = await useCase.execute({
      leagueId: 'league-1',
      seasonId: 'season-1',
    });

    expect(result.isErr()).toBe(true);
    const error = result.unwrapErr() as ApplicationErrorCode<GetSeasonDetailsErrorCode, { message: string }>;
    expect(error.code).toBe('SEASON_NOT_FOUND');
    expect(error.details?.message).toContain('does not belong to league');
  });
});

describe('ManageSeasonLifecycleUseCase', () => {
  function setup() {
    const leagueRepo = createFakeLeagueRepository([{ id: 'league-1' }]);
    const seasonRepo: SeasonRepository = {
      add: vi.fn(),
      findById: vi.fn(),
      update: vi.fn(),
      listByLeague: vi.fn(),
      listActiveByLeague: vi.fn(),
    } as unknown as SeasonRepository;

    const useCase = new ManageSeasonLifecycleUseCase(leagueRepo, seasonRepo);

    return { leagueRepo, seasonRepo, useCase };
  }

  it('applies activate → complete → archive transitions and persists state', async () => {
    const { seasonRepo, useCase } = setup();

    let currentSeason = Season.create({
      id: 'season-1',
      leagueId: 'league-1',
      gameId: 'iracing',
      name: 'Lifecycle Season',
      status: 'planned',
    });

    (seasonRepo.findById as unknown as ReturnType<typeof vi.fn>).mockImplementation(async () => currentSeason);
    (seasonRepo.update as unknown as ReturnType<typeof vi.fn>).mockImplementation(async (updated: Season) => {
      currentSeason = updated;
      return updated;
    });

    const activateCommand: ManageSeasonLifecycleCommand = {
      leagueId: 'league-1',
      seasonId: currentSeason.id,
      transition: 'activate',
    };

    const activated = await useCase.execute(activateCommand);
    expect(activated.isOk()).toBe(true);
    const activatePayload = activated.unwrap();
    expect(activatePayload.season.status.toString()).toBe('active');

    const completeCommand: ManageSeasonLifecycleCommand = {
      leagueId: 'league-1',
      seasonId: currentSeason.id,
      transition: 'complete',
    };

    const completed = await useCase.execute(completeCommand);
    expect(completed.isOk()).toBe(true);
    const completePayload = completed.unwrap();
    expect(completePayload.season.status.toString()).toBe('completed');

    const archiveCommand: ManageSeasonLifecycleCommand = {
      leagueId: 'league-1',
      seasonId: currentSeason.id,
      transition: 'archive',
    };

    const archived = await useCase.execute(archiveCommand);
    expect(archived.isOk()).toBe(true);
    const archivePayload = archived.unwrap();
    expect(archivePayload.season.status.toString()).toBe('archived');

    expect(currentSeason.status.toString()).toBe('archived');
  });

  it('returns INVALID_LIFECYCLE_TRANSITION for invalid transitions and does not call output', async () => {
    const { seasonRepo, useCase } = setup();

    const season = Season.create({
      id: 'season-1',
      leagueId: 'league-1',
      gameId: 'iracing',
      name: 'Lifecycle Season',
      status: 'planned',
    });

    (seasonRepo.findById as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(season);

    const invalidCommand: ManageSeasonLifecycleCommand = {
      leagueId: 'league-1',
      seasonId: season.id,
      transition: 'complete',
    };

    const result = await useCase.execute(invalidCommand);

    expect(result.isErr()).toBe(true);
    const error = result.unwrapErr() as ApplicationErrorCode<ManageSeasonLifecycleErrorCode, { message: string }>;
    expect(error.code).toBe('INVALID_LIFECYCLE_TRANSITION');
    expect(error.details?.message).toBeDefined();
  });

  it('returns LEAGUE_NOT_FOUND when league does not exist', async () => {
    const leagueRepo = createFakeLeagueRepository([]);
    const seasonRepo: SeasonRepository = {
      add: vi.fn(),
      findById: vi.fn(),
      update: vi.fn(),
      listByLeague: vi.fn(),
      listActiveByLeague: vi.fn(),
    } as unknown as SeasonRepository;

    const useCase = new ManageSeasonLifecycleUseCase(leagueRepo, seasonRepo);

    const command: ManageSeasonLifecycleCommand = {
      leagueId: 'missing-league',
      seasonId: 'season-1',
      transition: 'activate',
    };

    const result = await useCase.execute(command);

    expect(result.isErr()).toBe(true);
    const error = result.unwrapErr() as ApplicationErrorCode<ManageSeasonLifecycleErrorCode, { message: string }>;
    expect(error.code).toBe('LEAGUE_NOT_FOUND');
    expect(error.details?.message).toContain('League not found');
  });

  it('returns SEASON_NOT_FOUND when season does not belong to league', async () => {
    const leagueRepo = createFakeLeagueRepository([{ id: 'league-1' }]);
    const seasonRepo: SeasonRepository = {
      add: vi.fn(),
      findById: vi.fn(),
      update: vi.fn(),
      listByLeague: vi.fn(),
      listActiveByLeague: vi.fn(),
    } as unknown as SeasonRepository;

    const useCase = new ManageSeasonLifecycleUseCase(leagueRepo, seasonRepo);

    const season = Season.create({
      id: 'season-1',
      leagueId: 'other-league',
      gameId: 'iracing',
      name: 'Lifecycle Season',
      status: 'planned',
    });

    (seasonRepo.findById as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(season);

    const command: ManageSeasonLifecycleCommand = {
      leagueId: 'league-1',
      seasonId: season.id,
      transition: 'activate',
    };

    const result = await useCase.execute(command);

    expect(result.isErr()).toBe(true);
    const error = result.unwrapErr() as ApplicationErrorCode<ManageSeasonLifecycleErrorCode, { message: string }>;
    expect(error.code).toBe('SEASON_NOT_FOUND');
    expect(error.details?.message).toContain('does not belong to league');
  });
});
