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

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

vi.mock("~/database/schema", () => ({
  seasonParticipants: { id: "sp.id", sportsSeasonId: "sp.sports_season_id", name: "sp.name", shortName: "sp.short_name", externalId: "sp.external_id", participantId: "sp.participant_id" },
  seasonParticipantExpectedValues: { sportsSeasonId: "pev.sports_season_id" },
}));

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

import * as schema from "~/database/schema";
import { copyParticipantsFromSeason } from "../season-participant";
import { database } from "~/database/context";

const SOURCE_ID = "source-season";
const TARGET_ID = "target-season";

function makeSourceParticipant(overrides: Record<string, unknown> = {}) {
  return {
    id: "sp-src-1",
    sportsSeasonId: SOURCE_ID,
    name: "Tiger Woods",
    shortName: "T. Woods",
    externalId: "tiger",
    participantId: "canon-1",
    ...overrides,
  };
}

function makeSourceEv(overrides: Record<string, unknown> = {}) {
  return {
    id: "ev-src-1",
    participantId: "sp-src-1",
    sportsSeasonId: SOURCE_ID,
    sourceOdds: 800,
    sourceElo: null,
    source: "futures",
    worldRanking: 5,
    ...overrides,
  };
}

function makeMockDb({
  targetParticipants = [] as object[],
  sourceParticipants = [] as object[],
  sourceEvRows = [] as object[],
  newParticipantRows = sourceParticipants.map((p, i) => ({
    ...(p as Record<string, unknown>),
    id: `sp-new-${i + 1}`,
    sportsSeasonId: TARGET_ID,
  })),
}: {
  targetParticipants?: object[];
  sourceParticipants?: object[];
  sourceEvRows?: object[];
  newParticipantRows?: object[];
} = {}) {
  const insertedRows: Record<string, unknown> = {};

  const findManyCalls: { table: string; args: unknown }[] = [];

  const db = {
    query: {
      seasonParticipants: {
        findMany: vi.fn().mockImplementation((opts: unknown) => {
          findManyCalls.push({ table: "seasonParticipants", args: opts });
          const where = (opts as Record<string, unknown>)?.where as { val: string } | undefined;
          if (where?.val === SOURCE_ID) return sourceParticipants;
          if (where?.val === TARGET_ID) return targetParticipants;
          return targetParticipants;
        }),
      },
      seasonParticipantExpectedValues: {
        findMany: vi.fn().mockResolvedValue(sourceEvRows),
      },
    },
    insert: vi.fn().mockImplementation((table: object) => {
      let key: string;
      let returnRows: object[];
      if (table === schema.seasonParticipants) {
        key = "seasonParticipants";
        returnRows = newParticipantRows;
      } else {
        key = "participantExpectedValues";
        returnRows = [];
      }
      return {
        values: vi.fn().mockImplementation((vals: unknown) => {
          insertedRows[key] = vals;
          if (key === "seasonParticipants") {
            return { returning: vi.fn().mockResolvedValue(returnRows) };
          }
          return Promise.resolve(undefined);
        }),
      };
    }),
    transaction: vi.fn().mockImplementation(async (fn: (tx: typeof db) => Promise<unknown>) => fn(db)),
    _insertedRows: insertedRows,
    _findManyCalls: findManyCalls,
  };

  return db;
}

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

describe("copyParticipantsFromSeason", () => {
  it("returns empty result when source has no participants", async () => {
    const db = makeMockDb({ sourceParticipants: [] });
    vi.mocked(database).mockReturnValue(db as never);

    const result = await copyParticipantsFromSeason(SOURCE_ID, TARGET_ID);

    expect(result).toEqual({ copied: 0, skipped: 0, evStubsCopied: 0 });
    expect(db.transaction).not.toHaveBeenCalled();
  });

  it("skips participants already present in target (case-insensitive)", async () => {
    const db = makeMockDb({
      targetParticipants: [makeSourceParticipant({ id: "sp-tgt-1", sportsSeasonId: TARGET_ID })],
      sourceParticipants: [makeSourceParticipant()],
    });
    vi.mocked(database).mockReturnValue(db as never);

    const result = await copyParticipantsFromSeason(SOURCE_ID, TARGET_ID);

    expect(result).toEqual({ copied: 0, skipped: 1, evStubsCopied: 0 });
    expect(db.transaction).not.toHaveBeenCalled();
  });

  it("copies participants and returns counts", async () => {
    const srcP1 = makeSourceParticipant({ name: "Tiger Woods", externalId: "tiger", participantId: "canon-1" });
    const srcP2 = makeSourceParticipant({ id: "sp-src-2", name: "Rory McIlroy", externalId: "rory", participantId: "canon-2" });
    const db = makeMockDb({
      sourceParticipants: [srcP1, srcP2],
      newParticipantRows: [
        { ...srcP1, id: "sp-new-1", sportsSeasonId: TARGET_ID },
        { ...srcP2, id: "sp-new-2", sportsSeasonId: TARGET_ID },
      ],
    });
    vi.mocked(database).mockReturnValue(db as never);

    const result = await copyParticipantsFromSeason(SOURCE_ID, TARGET_ID);

    expect(result.copied).toBe(2);
    expect(result.skipped).toBe(0);
    expect(db._insertedRows.seasonParticipants).toHaveLength(2);
    const values = db._insertedRows.seasonParticipants as Array<Record<string, unknown>>;
    expect(values[0]).toMatchObject({ name: "Tiger Woods", externalId: "tiger", participantId: "canon-1" });
    expect(values[1]).toMatchObject({ name: "Rory McIlroy", externalId: "rory", participantId: "canon-2" });
  });

  it("copies EV stubs matched by externalId", async () => {
    const srcP = makeSourceParticipant({ externalId: "tiger" });
    const srcEv = makeSourceEv({ participantId: "sp-src-1", sourceOdds: 800 });
    const db = makeMockDb({
      sourceParticipants: [srcP],
      sourceEvRows: [srcEv],
      newParticipantRows: [{ ...srcP, id: "sp-new-1", sportsSeasonId: TARGET_ID }],
    });
    vi.mocked(database).mockReturnValue(db as never);

    const result = await copyParticipantsFromSeason(SOURCE_ID, TARGET_ID);

    expect(result.evStubsCopied).toBe(1);
    const evRows = db._insertedRows.participantExpectedValues as Array<Record<string, unknown>>;
    expect(evRows).toHaveLength(1);
    expect(evRows[0]).toMatchObject({
      participantId: "sp-new-1",
      sourceOdds: 800,
      source: "futures",
      worldRanking: 5,
    });
  });

  it("copies EV stubs matched by name when externalId is null", async () => {
    const srcP = makeSourceParticipant({ externalId: null, name: "Tiger Woods" });
    const srcEv = makeSourceEv({ participantId: "sp-src-1" });
    const db = makeMockDb({
      sourceParticipants: [srcP],
      sourceEvRows: [srcEv],
      newParticipantRows: [{ ...srcP, id: "sp-new-1", sportsSeasonId: TARGET_ID }],
    });
    vi.mocked(database).mockReturnValue(db as never);

    const result = await copyParticipantsFromSeason(SOURCE_ID, TARGET_ID);

    expect(result.evStubsCopied).toBe(1);
  });

  it("skips EV rows with neither sourceOdds nor sourceElo", async () => {
    const srcP = makeSourceParticipant();
    const srcEv = makeSourceEv({ sourceOdds: null, sourceElo: null });
    const db = makeMockDb({
      sourceParticipants: [srcP],
      sourceEvRows: [srcEv],
      newParticipantRows: [{ ...srcP, id: "sp-new-1", sportsSeasonId: TARGET_ID }],
    });
    vi.mocked(database).mockReturnValue(db as never);

    const result = await copyParticipantsFromSeason(SOURCE_ID, TARGET_ID);

    expect(result.evStubsCopied).toBe(0);
    expect(db._insertedRows.participantExpectedValues).toBeUndefined();
  });

  it("wraps writes in a transaction", async () => {
    const srcP = makeSourceParticipant();
    const db = makeMockDb({
      sourceParticipants: [srcP],
      newParticipantRows: [{ ...srcP, id: "sp-new-1", sportsSeasonId: TARGET_ID }],
    });
    vi.mocked(database).mockReturnValue(db as never);

    await copyParticipantsFromSeason(SOURCE_ID, TARGET_ID);

    expect(db.transaction).toHaveBeenCalledOnce();
  });

  it("handles partial duplicates (some copied, some skipped)", async () => {
    const srcP1 = makeSourceParticipant({ name: "Tiger Woods" });
    const srcP2 = makeSourceParticipant({ id: "sp-src-2", name: "Rory McIlroy", participantId: "canon-2" });
    const db = makeMockDb({
      targetParticipants: [makeSourceParticipant({ id: "sp-tgt-1", sportsSeasonId: TARGET_ID, name: "Tiger Woods" })],
      sourceParticipants: [srcP1, srcP2],
      newParticipantRows: [{ ...srcP2, id: "sp-new-1", sportsSeasonId: TARGET_ID }],
    });
    vi.mocked(database).mockReturnValue(db as never);

    const result = await copyParticipantsFromSeason(SOURCE_ID, TARGET_ID);

    expect(result.copied).toBe(1);
    expect(result.skipped).toBe(1);
    const values = db._insertedRows.seasonParticipants as Array<Record<string, unknown>>;
    expect(values).toHaveLength(1);
    expect(values[0]).toMatchObject({ name: "Rory McIlroy" });
  });
});
