import { describe, it, expect, vi, beforeEach } from "vitest";

vi.mock("~/database/context", () => ({
  database: vi.fn(),
}));

vi.mock("~/database/schema", () => ({
  sportsSeasons: { id: "ss.id", sportId: "ss.sport_id" },
  seasonParticipants: { sportsSeasonId: "p.sports_season_id" },
  scoringEvents: { sportsSeasonId: "se.sports_season_id" },
  seasonParticipantExpectedValues: { sportsSeasonId: "pev.sports_season_id" },
  sportsSeasonSimulatorConfigs: { sportsSeasonId: "ssc.sports_season_id" },
  seasonParticipantSimulatorInputs: { sportsSeasonId: "spi.sports_season_id" },
}));

vi.mock("drizzle-orm", () => ({
  eq: (col: unknown, val: unknown) => ({ type: "eq", col, val }),
  sql: (strings: TemplateStringsArray, ...values: unknown[]) => ({ type: "sql", strings, values }),
  lte: (col: unknown, val: unknown) => ({ type: "lte", col, val }),
  gte: (col: unknown, val: unknown) => ({ type: "gte", col, val }),
  and: (...args: unknown[]) => ({ type: "and", args }),
  desc: (col: unknown) => ({ type: "desc", col }),
  asc: (col: unknown) => ({ type: "asc", col }),
}));

vi.mock("~/models/qualifying-points", () => ({
  getQPConfig: vi.fn(),
  updateQPConfig: vi.fn(),
}));

import * as schema from "~/database/schema";
import { cloneSportsSeason, type NewSportsSeason } from "../sports-season";
import { database } from "~/database/context";
import { getQPConfig, updateQPConfig } from "~/models/qualifying-points";

const SOURCE_ID = "source-season-id";
const NEW_ID = "new-season-id";

const sourceSeason = {
  id: SOURCE_ID,
  sportId: "sport-1",
  name: "2025 F1 Season",
  year: 2025,
  startDate: "2025-03-16",
  endDate: "2025-12-07",
  draftOn: "2025-01-01",
  draftOff: "2025-03-15",
  status: "completed",
  scoringType: "majors",
  scoringPattern: "season_standings",
  totalMajors: null,
  majorsCompleted: 5,
  qualifyingPointsFinalized: true,
  eloCalibrationExponent: "0.33",
  eloMinRating: 1250,
  eloMaxRating: 1750,
  simulationStatus: "idle",
};

const newSeasonData: NewSportsSeason = {
  sportId: "sport-1",
  name: "2026 F1 Season",
  year: 2026,
  startDate: "2026-03-16",
  endDate: "2026-12-07",
  draftOn: "2026-01-01",
  draftOff: "2026-03-15",
  status: "upcoming",
  simulationStatus: "idle",
  majorsCompleted: 0,
  qualifyingPointsFinalized: false,
  scoringType: "majors",
  scoringPattern: "season_standings",
};

function makeMockDb({
  sourceSeason: ss = sourceSeason,
  participants = [] as object[],
  scoringEvents = [] as object[],
  sourceEvRows = [] as object[],
  sourceSimulatorConfig = null as object | null,
  sourceSimulatorInputs = [] as object[],
  insertedSeason = { ...newSeasonData, id: NEW_ID } as object,
  // New participants returned by the INSERT ... RETURNING — mirrors sources with new IDs by default
  newParticipantRows = (participants as Array<Record<string, unknown>>).map((p, i) => ({
    ...p,
    id: `new-p${i + 1}`,
    sportsSeasonId: NEW_ID,
  })) as object[],
} = {}) {
  // Track what was inserted into each table independently of call order
  const insertedRows: Record<string, unknown> = {};

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const db: any = {
    query: {
      sportsSeasons: { findFirst: vi.fn().mockResolvedValue(ss) },
      seasonParticipants: { findMany: vi.fn().mockResolvedValue(participants) },
      scoringEvents: { findMany: vi.fn().mockResolvedValue(scoringEvents) },
      seasonParticipantExpectedValues: { findMany: vi.fn().mockResolvedValue(sourceEvRows) },
      sportsSeasonSimulatorConfigs: { findFirst: vi.fn().mockResolvedValue(sourceSimulatorConfig) },
      seasonParticipantSimulatorInputs: { findMany: vi.fn().mockResolvedValue(sourceSimulatorInputs) },
    },
    insert: vi.fn().mockImplementation((table: object) => {
      let key: string;
      let returnRows: object[];
      if (table === schema.sportsSeasons) {
        key = "sportsSeasons"; returnRows = [insertedSeason];
      } else if (table === schema.seasonParticipants) {
        key = "participants"; returnRows = newParticipantRows;
      } else if (table === schema.scoringEvents) {
        key = "scoringEvents"; returnRows = [];
      } else if (table === schema.sportsSeasonSimulatorConfigs) {
        key = "sportsSeasonSimulatorConfigs"; returnRows = [];
      } else if (table === schema.seasonParticipantSimulatorInputs) {
        key = "seasonParticipantSimulatorInputs"; returnRows = [];
      } else {
        key = "participantExpectedValues"; returnRows = [];
      }
      return {
        values: vi.fn().mockImplementation((vals: unknown) => {
          insertedRows[key] = vals;
          return { returning: vi.fn().mockResolvedValue(returnRows) };
        }),
      };
    }),
    // Passes `db` as the transaction object so tracked inserts still work
    transaction: vi.fn().mockImplementation(async (fn: (tx: typeof db) => Promise<unknown>) => fn(db)),
    _insertedRows: insertedRows,
  };

  return db;
}

beforeEach(() => {
  vi.clearAllMocks();
});

describe("cloneSportsSeason", () => {
  it("creates new season with reset fields (status=upcoming, majorsCompleted=0, simulationStatus=idle)", async () => {
    const db = makeMockDb();
    vi.mocked(database).mockReturnValue(db as never);

    await cloneSportsSeason(SOURCE_ID, newSeasonData, { copySimulatorInputs: true });

    expect(db._insertedRows.sportsSeasons).toMatchObject({
      name: "2026 F1 Season",
      year: 2026,
      status: "upcoming",
      simulationStatus: "idle",
      majorsCompleted: 0,
      qualifyingPointsFinalized: false,
    });
  });

  it("merges ELO calibration fields from source season", async () => {
    const db = makeMockDb();
    vi.mocked(database).mockReturnValue(db as never);

    await cloneSportsSeason(SOURCE_ID, newSeasonData, { copySimulatorInputs: true });

    expect(db._insertedRows.sportsSeasons).toMatchObject({
      eloCalibrationExponent: "0.33",
      eloMinRating: 1250,
      eloMaxRating: 1750,
    });
  });

  it("copies participants with name, shortName, externalId — not expectedValue", async () => {
    const participants = [
      { id: "p1", name: "Max Verstappen", shortName: "VER", externalId: "ext-1", expectedValue: "42.5" },
      { id: "p2", name: "Lewis Hamilton", shortName: "HAM", externalId: "ext-2", expectedValue: "38.0" },
    ];
    const db = makeMockDb({ participants });
    vi.mocked(database).mockReturnValue(db as never);

    await cloneSportsSeason(SOURCE_ID, newSeasonData, { copySimulatorInputs: true });

    const rows = db._insertedRows.participants as object[];
    expect(rows).toHaveLength(2);
    expect(rows[0]).toMatchObject({
      sportsSeasonId: NEW_ID,
      name: "Max Verstappen",
      shortName: "VER",
      externalId: "ext-1",
    });
    expect(rows[0]).not.toHaveProperty("expectedValue");
    expect(rows[0]).not.toHaveProperty("id");
  });

  it("skips participant insert when source has no participants", async () => {
    const db = makeMockDb({ participants: [] });
    vi.mocked(database).mockReturnValue(db as never);

    await cloneSportsSeason(SOURCE_ID, newSeasonData, { copySimulatorInputs: true });

    expect(db._insertedRows.participants).toBeUndefined();
  });

  it("copies scoring events with dates shifted by yearDelta", async () => {
    const scoringEvents = [
      { id: "e1", name: "Bahrain GP", eventType: "final_standings", isQualifyingEvent: false,
        bracketTemplateId: null, scoringStartsAtRound: null,
        eventDate: "2025-03-16", eventStartsAt: null },
      { id: "e2", name: "Abu Dhabi GP", eventType: "final_standings", isQualifyingEvent: false,
        bracketTemplateId: null, scoringStartsAtRound: null,
        eventDate: "2025-12-07", eventStartsAt: null },
    ];
    const db = makeMockDb({ scoringEvents });
    vi.mocked(database).mockReturnValue(db as never);

    await cloneSportsSeason(SOURCE_ID, newSeasonData, { copySimulatorInputs: true });

    const rows = db._insertedRows.scoringEvents as object[];
    expect(rows).toHaveLength(2);
    expect(rows[0]).toMatchObject({
      sportsSeasonId: NEW_ID,
      name: "Bahrain GP",
      eventDate: "2026-03-16",
      isComplete: false,
    });
    expect(rows[1]).toMatchObject({
      name: "Abu Dhabi GP",
      eventDate: "2026-12-07",
    });
  });

  it("leaves eventDate null when source event has no date", async () => {
    const scoringEvents = [
      { id: "e1", name: "TBD Event", eventType: "schedule_event", isQualifyingEvent: false,
        bracketTemplateId: null, scoringStartsAtRound: null,
        eventDate: null, eventStartsAt: null },
    ];
    const db = makeMockDb({ scoringEvents });
    vi.mocked(database).mockReturnValue(db as never);

    await cloneSportsSeason(SOURCE_ID, newSeasonData, { copySimulatorInputs: true });

    const rows = db._insertedRows.scoringEvents as Array<{ eventDate: unknown }>;
    expect(rows[0].eventDate).toBeNull();
  });

  it("shifts eventStartsAt timestamp by yearDelta", async () => {
    const originalDate = new Date("2025-06-15T14:00:00Z");
    const scoringEvents = [
      { id: "e1", name: "Mid-year Event", eventType: "playoff_game", isQualifyingEvent: false,
        bracketTemplateId: null, scoringStartsAtRound: null,
        eventDate: null, eventStartsAt: originalDate },
    ];
    const db = makeMockDb({ scoringEvents });
    vi.mocked(database).mockReturnValue(db as never);

    await cloneSportsSeason(SOURCE_ID, newSeasonData, { copySimulatorInputs: true });

    const rows = db._insertedRows.scoringEvents as Array<{ eventStartsAt: Date }>;
    const shiftedDate = rows[0].eventStartsAt;
    expect(shiftedDate.getUTCFullYear()).toBe(2026);
    expect(shiftedDate.getUTCMonth()).toBe(originalDate.getUTCMonth());
    expect(shiftedDate.getUTCDate()).toBe(originalDate.getUTCDate());
  });

  it("copies futures odds as stub EV records matched by externalId", async () => {
    const participants = [
      { id: "src-p1", name: "Max Verstappen", shortName: "VER", externalId: "ext-1" },
      { id: "src-p2", name: "Lewis Hamilton", shortName: "HAM", externalId: "ext-2" },
    ];
    const sourceEvRows = [
      { participantId: "src-p1", sportsSeasonId: SOURCE_ID,
        source: "futures_odds", sourceOdds: -120, sourceElo: null, worldRanking: null },
      { participantId: "src-p2", sportsSeasonId: SOURCE_ID,
        source: "futures_odds", sourceOdds: 300, sourceElo: null, worldRanking: null },
    ];
    const newParticipantRows = [
      { id: "new-p1", name: "Max Verstappen", externalId: "ext-1", sportsSeasonId: NEW_ID },
      { id: "new-p2", name: "Lewis Hamilton", externalId: "ext-2", sportsSeasonId: NEW_ID },
    ];
    const db = makeMockDb({ participants, sourceEvRows, newParticipantRows });
    vi.mocked(database).mockReturnValue(db as never);

    await cloneSportsSeason(SOURCE_ID, newSeasonData, { copySimulatorInputs: true });

    const evRows = db._insertedRows.participantExpectedValues as Array<Record<string, unknown>>;
    expect(evRows).toHaveLength(2);
    expect(evRows[0]).toMatchObject({
      participantId: "new-p1",
      sportsSeasonId: NEW_ID,
      source: "futures_odds",
      sourceOdds: -120,
      sourceElo: null,
      expectedValue: "0",
      probFirst: "0",
    });
    expect(evRows[1]).toMatchObject({ participantId: "new-p2", sourceOdds: 300 });
  });

  it("copies Elo ratings and world rankings as stub EV records", async () => {
    const participants = [
      { id: "src-p1", name: "Player A", shortName: "PA", externalId: "ext-1" },
    ];
    const sourceEvRows = [
      { participantId: "src-p1", sportsSeasonId: SOURCE_ID,
        source: "elo_simulation", sourceOdds: null, sourceElo: 1650, worldRanking: 3 },
    ];
    const newParticipantRows = [
      { id: "new-p1", name: "Player A", externalId: "ext-1", sportsSeasonId: NEW_ID },
    ];
    const db = makeMockDb({ participants, sourceEvRows, newParticipantRows });
    vi.mocked(database).mockReturnValue(db as never);

    await cloneSportsSeason(SOURCE_ID, newSeasonData, { copySimulatorInputs: true });

    const evRows = db._insertedRows.participantExpectedValues as Array<Record<string, unknown>>;
    expect(evRows).toHaveLength(1);
    expect(evRows[0]).toMatchObject({
      participantId: "new-p1",
      source: "elo_simulation",
      sourceElo: 1650,
      worldRanking: 3,
      sourceOdds: null,
    });
  });

  it("falls back to name matching when externalId is null", async () => {
    const participants = [
      { id: "src-p1", name: "Player A", shortName: "PA", externalId: null },
    ];
    const sourceEvRows = [
      { participantId: "src-p1", sportsSeasonId: SOURCE_ID,
        source: "futures_odds", sourceOdds: 200, sourceElo: null, worldRanking: null },
    ];
    const newParticipantRows = [
      { id: "new-p1", name: "Player A", externalId: null, sportsSeasonId: NEW_ID },
    ];
    const db = makeMockDb({ participants, sourceEvRows, newParticipantRows });
    vi.mocked(database).mockReturnValue(db as never);

    await cloneSportsSeason(SOURCE_ID, newSeasonData, { copySimulatorInputs: true });

    const evRows = db._insertedRows.participantExpectedValues as Array<Record<string, unknown>>;
    expect(evRows).toHaveLength(1);
    expect(evRows[0]).toMatchObject({ participantId: "new-p1", sourceOdds: 200 });
  });

  it("skips EV copy when source has no odds or Elo data", async () => {
    const participants = [
      { id: "src-p1", name: "Player A", shortName: "PA", externalId: "ext-1" },
    ];
    // EV row exists but has no input data — e.g. only calculated probabilities
    const sourceEvRows = [
      { participantId: "src-p1", sportsSeasonId: SOURCE_ID,
        source: "manual", sourceOdds: null, sourceElo: null, worldRanking: null },
    ];
    const db = makeMockDb({ participants, sourceEvRows });
    vi.mocked(database).mockReturnValue(db as never);

    await cloneSportsSeason(SOURCE_ID, newSeasonData, { copySimulatorInputs: true });

    expect(db._insertedRows.participantExpectedValues).toBeUndefined();
  });

  it("skips EV copy when source season has no EV rows", async () => {
    const participants = [
      { id: "src-p1", name: "Player A", shortName: "PA", externalId: "ext-1" },
    ];
    const db = makeMockDb({ participants, sourceEvRows: [] });
    vi.mocked(database).mockReturnValue(db as never);

    await cloneSportsSeason(SOURCE_ID, newSeasonData);

    expect(db._insertedRows.participantExpectedValues).toBeUndefined();
  });

  it("copies QP config when scoringPattern is qualifying_points", async () => {
    const qpSeason: NewSportsSeason = {
      ...newSeasonData,
      scoringPattern: "qualifying_points",
      totalMajors: 4,
    };
    const mockQPConfig = [
      { placement: 1, points: "20" },
      { placement: 2, points: "14" },
    ];
    vi.mocked(getQPConfig).mockResolvedValue(mockQPConfig as never);
    vi.mocked(updateQPConfig).mockResolvedValue([] as never);

    const db = makeMockDb();
    vi.mocked(database).mockReturnValue(db as never);

    await cloneSportsSeason(SOURCE_ID, qpSeason);

    expect(getQPConfig).toHaveBeenCalledWith(SOURCE_ID, db);
    expect(updateQPConfig).toHaveBeenCalledWith(
      NEW_ID,
      [
        { placement: 1, points: 20 },
        { placement: 2, points: 14 },
      ],
      db
    );
  });

  it("does NOT copy QP config for non-QP seasons", async () => {
    const db = makeMockDb();
    vi.mocked(database).mockReturnValue(db as never);

    await cloneSportsSeason(SOURCE_ID, newSeasonData);

    expect(getQPConfig).not.toHaveBeenCalled();
    expect(updateQPConfig).not.toHaveBeenCalled();
  });

  it("throws when source season is not found", async () => {
    const db = makeMockDb();
    db.query.sportsSeasons.findFirst = vi.fn().mockResolvedValue(undefined);
    vi.mocked(database).mockReturnValue(db as never);

    await expect(cloneSportsSeason("nonexistent-id", newSeasonData)).rejects.toThrow(
      "Source season nonexistent-id not found"
    );
  });
});
