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

vi.mock("~/lib/logger", () => ({
  logger: { error: vi.fn() },
}));

// ── DB mock helpers ────────────────────────────────────────────────────────

function makeInsertChain() {
  const onConflictDoUpdate = vi.fn().mockResolvedValue(undefined);
  const values = vi.fn().mockReturnValue({ onConflictDoUpdate });
  const insert = vi.fn().mockReturnValue({ values });
  return { insert, values, onConflictDoUpdate };
}

interface MakeDbOpts {
  sportsSeason?: { sport: { name: string } } | null;
  seasonSports?: { seasonId: string }[];
  picks?: { teamId: string; seasonId: string }[];
  seasons?: {
    id: string;
    pointsFor1st: number; pointsFor2nd: number; pointsFor3rd: number;
    pointsFor4th: number; pointsFor5th: number; pointsFor6th: number;
    pointsFor7th: number; pointsFor8th: number;
  }[];
  scoreEventRows?: {
    id: string; teamId: string; teamName?: string;
    scoringEventId: string | null; scoringEventName: string | null;
    sportName: string | null; pointsDelta: string; occurredAt: Date;
    participantIds: string[];
    team: { id: string; name: string };
  }[];
  participantRows?: { id: string; name: string }[];
}

function makeDb(opts: MakeDbOpts = {}) {
  const {
    sportsSeason = null,
    seasonSports = [],
    picks = [],
    seasons = [],
    scoreEventRows = [],
    participantRows = [],
  } = opts;

  const chain = makeInsertChain();

  return {
    db: {
      insert: chain.insert,
      query: {
        sportsSeasons: {
          findFirst: vi.fn().mockResolvedValue(sportsSeason),
        },
        seasonSports: {
          findMany: vi.fn().mockResolvedValue(seasonSports),
        },
        draftPicks: {
          findMany: vi.fn().mockResolvedValue(picks),
        },
        seasons: {
          findMany: vi.fn().mockResolvedValue(seasons),
        },
        teamScoreEvents: {
          findMany: vi.fn().mockResolvedValue(scoreEventRows),
        },
        seasonParticipants: {
          findMany: vi.fn().mockResolvedValue(participantRows),
        },
      },
    } as any,
    chain,
  };
}

import { recordTeamScoreEvent, recordMatchScoreEvents, getRecentTeamScoreEvents } from "../team-score-events";

const BASE_PARAMS = {
  teamId: "team-1",
  seasonId: "season-1",
  scoringEventId: "event-1",
  scoringEventName: "Round of 16",
  sportName: "Darts",
  participantIds: ["p-1"],
  pointsDelta: 10,
};

describe("recordTeamScoreEvent", () => {
  beforeEach(() => vi.clearAllMocks());

  it("uses matchId-based conflict target when matchId provided", async () => {
    const { db, chain } = makeDb();

    await recordTeamScoreEvent({ ...BASE_PARAMS, matchId: "match-1" }, db);

    expect(chain.insert).toHaveBeenCalled();
    expect(chain.values).toHaveBeenCalledWith(
      expect.objectContaining({ matchId: "match-1", pointsDelta: "10" })
    );
    const conflictArg = chain.onConflictDoUpdate.mock.calls[0][0];
    expect(conflictArg.target).toHaveLength(3);
    // targetWhere should reference matchId IS NOT NULL
    expect(conflictArg.targetWhere).toBeDefined();
    expect(conflictArg.set).toMatchObject({ pointsDelta: "10", participantIds: ["p-1"] });
  });

  it("uses scoringEventId-based conflict target when no matchId", async () => {
    const { db, chain } = makeDb();

    await recordTeamScoreEvent(BASE_PARAMS, db);

    expect(chain.insert).toHaveBeenCalled();
    expect(chain.values).toHaveBeenCalledWith(
      expect.objectContaining({ matchId: null })
    );
    const conflictArg = chain.onConflictDoUpdate.mock.calls[0][0];
    expect(conflictArg.target).toHaveLength(3);
    expect(conflictArg.targetWhere).toBeDefined();
  });

  it("stores pointsDelta as string", async () => {
    const { db, chain } = makeDb();
    await recordTeamScoreEvent({ ...BASE_PARAMS, pointsDelta: 42 }, db);
    expect(chain.values).toHaveBeenCalledWith(expect.objectContaining({ pointsDelta: "42" }));
  });

  it("defaults matchId to null when not provided", async () => {
    const { db, chain } = makeDb();
    await recordTeamScoreEvent(BASE_PARAMS, db);
    expect(chain.values).toHaveBeenCalledWith(expect.objectContaining({ matchId: null }));
  });
});

describe("recordMatchScoreEvents", () => {
  beforeEach(() => vi.clearAllMocks());

  const BASE_EVENT = {
    participantId: "p-1",
    sportsSeasonId: "ss-1",
    oldFloor: 0,
    newFloor: 2,
    bracketTemplateId: null as string | null,
    matchId: "match-1",
    eventId: "event-1",
    eventName: "Semifinals",
  };

  const SEASON_ROW = {
    id: "season-1",
    pointsFor1st: 100, pointsFor2nd: 75, pointsFor3rd: 50,
    pointsFor4th: 40, pointsFor5th: 30, pointsFor6th: 20,
    pointsFor7th: 10, pointsFor8th: 5,
  };

  it("returns early when no fantasy seasons use this sports season", async () => {
    const { db, chain } = makeDb({
      sportsSeason: { sport: { name: "Darts" } },
      seasonSports: [],
    });

    await recordMatchScoreEvents(BASE_EVENT, db);

    expect(chain.insert).not.toHaveBeenCalled();
  });

  it("returns early when no draft picks found for participant", async () => {
    const { db, chain } = makeDb({
      sportsSeason: { sport: { name: "Darts" } },
      seasonSports: [{ seasonId: "season-1" }],
      picks: [],
      seasons: [SEASON_ROW],
    });

    await recordMatchScoreEvents(BASE_EVENT, db);

    expect(chain.insert).not.toHaveBeenCalled();
  });

  it("skips seasons where team did not draft the participant", async () => {
    const { db, chain } = makeDb({
      sportsSeason: { sport: { name: "Darts" } },
      seasonSports: [{ seasonId: "season-1" }, { seasonId: "season-2" }],
      picks: [{ teamId: "team-1", seasonId: "season-1" }], // only season-1 has pick
      seasons: [SEASON_ROW, { ...SEASON_ROW, id: "season-2" }],
    });

    await recordMatchScoreEvents(BASE_EVENT, db);

    expect(chain.insert).toHaveBeenCalledTimes(1);
    expect(chain.values).toHaveBeenCalledWith(expect.objectContaining({ seasonId: "season-1" }));
  });

  it("skips seasons where point delta is zero or negative", async () => {
    const { db, chain } = makeDb({
      sportsSeason: { sport: { name: "Darts" } },
      seasonSports: [{ seasonId: "season-1" }],
      picks: [{ teamId: "team-1", seasonId: "season-1" }],
      seasons: [{ ...SEASON_ROW, pointsFor1st: 0, pointsFor2nd: 0 }], // all zeros → delta=0
    });

    // With all-zero scoring rules, positions map to 0 points → delta = 0 → skip
    await recordMatchScoreEvents({ ...BASE_EVENT, oldFloor: 1, newFloor: 2 }, db);

    expect(chain.insert).not.toHaveBeenCalled();
  });

  it("records score event with correct teamId, seasonId, and matchId", async () => {
    const { db, chain } = makeDb({
      sportsSeason: { sport: { name: "Darts" } },
      seasonSports: [{ seasonId: "season-1" }],
      picks: [{ teamId: "team-1", seasonId: "season-1" }],
      seasons: [SEASON_ROW],
    });

    await recordMatchScoreEvents(BASE_EVENT, db);

    expect(chain.values).toHaveBeenCalledWith(
      expect.objectContaining({
        teamId: "team-1",
        seasonId: "season-1",
        matchId: "match-1",
        sportName: "Darts",
        participantIds: ["p-1"],
      })
    );
  });

  it("uses sport name from sportsSeasons query", async () => {
    const { db, chain } = makeDb({
      sportsSeason: { sport: { name: "Snooker" } },
      seasonSports: [{ seasonId: "season-1" }],
      picks: [{ teamId: "team-1", seasonId: "season-1" }],
      seasons: [SEASON_ROW],
    });

    await recordMatchScoreEvents(BASE_EVENT, db);

    expect(chain.values).toHaveBeenCalledWith(
      expect.objectContaining({ sportName: "Snooker" })
    );
  });

  it("handles null sportsSeason gracefully (sportName = null)", async () => {
    const { db, chain } = makeDb({
      sportsSeason: null,
      seasonSports: [{ seasonId: "season-1" }],
      picks: [{ teamId: "team-1", seasonId: "season-1" }],
      seasons: [SEASON_ROW],
    });

    await recordMatchScoreEvents(BASE_EVENT, db);

    expect(chain.values).toHaveBeenCalledWith(
      expect.objectContaining({ sportName: null })
    );
  });
});

describe("getRecentTeamScoreEvents", () => {
  beforeEach(() => vi.clearAllMocks());

  it("returns empty array when no rows found", async () => {
    const { db } = makeDb({ scoreEventRows: [] });
    const result = await getRecentTeamScoreEvents("season-1", 10, db);
    expect(result).toEqual([]);
  });

  it("does not query participants when rows array is empty", async () => {
    const { db } = makeDb({ scoreEventRows: [] });
    await getRecentTeamScoreEvents("season-1", 10, db);
    expect(db.query.seasonParticipants.findMany).not.toHaveBeenCalled();
  });

  it("returns mapped entries with resolved participant names", async () => {
    const occurredAt = new Date("2024-01-15");
    const { db } = makeDb({
      scoreEventRows: [
        {
          id: "tse-1",
          teamId: "team-1",
          scoringEventId: "event-1",
          scoringEventName: "Semifinals",
          sportName: "Darts",
          pointsDelta: "75",
          occurredAt,
          participantIds: ["p-1", "p-2"],
          team: { id: "team-1", name: "Team Alpha" },
        },
      ],
      participantRows: [
        { id: "p-1", name: "Luke Littler" },
        { id: "p-2", name: "Michael van Gerwen" },
      ],
    });

    const result = await getRecentTeamScoreEvents("season-1", 10, db);

    expect(result).toHaveLength(1);
    expect(result[0]).toMatchObject({
      id: "tse-1",
      teamId: "team-1",
      teamName: "Team Alpha",
      scoringEventId: "event-1",
      scoringEventName: "Semifinals",
      sportName: "Darts",
      pointsDelta: "75",
      occurredAt,
    });
    expect(result[0].participants).toEqual([
      { id: "p-1", name: "Luke Littler" },
      { id: "p-2", name: "Michael van Gerwen" },
    ]);
  });

  it("omits participants whose names cannot be resolved", async () => {
    const { db } = makeDb({
      scoreEventRows: [
        {
          id: "tse-1",
          teamId: "team-1",
          scoringEventId: "event-1",
          scoringEventName: null,
          sportName: null,
          pointsDelta: "50",
          occurredAt: new Date(),
          participantIds: ["p-1", "p-missing"],
          team: { id: "team-1", name: "Team Beta" },
        },
      ],
      participantRows: [{ id: "p-1", name: "Known Player" }],
    });

    const result = await getRecentTeamScoreEvents("season-1", 10, db);

    expect(result[0].participants).toEqual([{ id: "p-1", name: "Known Player" }]);
  });

  it("handles null participantIds gracefully", async () => {
    const { db } = makeDb({
      scoreEventRows: [
        {
          id: "tse-1",
          teamId: "team-1",
          scoringEventId: "event-1",
          scoringEventName: null,
          sportName: null,
          pointsDelta: "10",
          occurredAt: new Date(),
          participantIds: null as any,
          team: { id: "team-1", name: "Team Gamma" },
        },
      ],
      participantRows: [],
    });

    const result = await getRecentTeamScoreEvents("season-1", 10, db);

    expect(result[0].participants).toEqual([]);
  });
});
