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

// Mock external DB-touching dependencies before importing the module under test
vi.mock("~/services/probability-updater", () => ({
  updateProbabilitiesAfterResult: vi.fn().mockResolvedValue(undefined),
}));

// ── Minimal db mock ────────────────────────────────────────────────────────
//
// processMatchResult calls, in order:
//   1. upsertParticipantResult (x2) → db.query.participantResults.findFirst, db.insert
//   2. recalculateAffectedLeagues   → db.query.seasonSports.findMany (returns [] so loop exits)
//   3. updateProbabilitiesAfterResult (mocked above)

function makeDb(existingResult?: { id: string; isPartialScore: boolean }) {
  // Track all inserted values in order
  const insertedRows: object[] = [];
  const updatedRows: object[] = [];

  const insertValues = vi.fn().mockImplementation((values: object) => {
    insertedRows.push(values);
    return Promise.resolve();
  });

  const updateWhere = vi.fn().mockResolvedValue(undefined);
  const updateSet = vi.fn().mockImplementation((values: object) => {
    updatedRows.push(values);
    return { where: updateWhere };
  });

  // participantResults.findFirst: first call gets existingResult (if any),
  // subsequent calls return undefined (winner/loser don't have existing rows)
  let findFirstCallCount = 0;
  const findFirst = vi.fn().mockImplementation(() => {
    const result = findFirstCallCount === 0 ? existingResult : undefined;
    findFirstCallCount++;
    return Promise.resolve(result);
  });

  return {
    db: {
      insert: vi.fn().mockReturnValue({ values: insertValues }),
      update: vi.fn().mockReturnValue({ set: updateSet }),
      query: {
        seasonParticipantResults: { findFirst },
        seasonSports: {
          findMany: vi.fn().mockResolvedValue([]), // empty → standings loop exits immediately
        },
        sportsSeasons: { findFirst: vi.fn().mockResolvedValue(null) },
        scoringEvents: { findFirst: vi.fn().mockResolvedValue(null) },
      },
    } as any,
    insertedRows,
    updatedRows,
    findFirst,
    insertValues,
    updateSet,
    updateWhere,
  };
}

import { processMatchResult, isLoserNotifiable, processPlayoffEvent } from "../scoring-calculator";
import { doesLoserAdvance } from "../playoff-match";
import { updateProbabilitiesAfterResult } from "~/services/probability-updater";

const BASE = {
  winnerId: "winner-1",
  loserId: "loser-1",
  sportsSeasonId: "ss-1",
  bracketTemplateId: null as string | null,
};

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

  describe("Quarterfinals (scoring)", () => {
    it("loser gets position 5 (final), winner gets position 3 (provisional)", async () => {
      const { db, insertedRows } = makeDb();

      await processMatchResult(
        { ...BASE, round: "Quarterfinals", isScoring: true },
        db
      );

      expect(insertedRows).toHaveLength(2);
      expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 5, isPartialScore: false });
      expect(insertedRows[1]).toMatchObject({ participantId: "winner-1", finalPosition: 3, isPartialScore: true });
    });

    it("also works for Elite Eight / Divisional / Conference Semifinals", async () => {
      for (const round of ["Elite Eight", "Divisional", "Conference Semifinals"]) {
        const { db, insertedRows } = makeDb();
        await processMatchResult({ ...BASE, round, isScoring: true }, db);
        expect(insertedRows[0]).toMatchObject({ finalPosition: 5, isPartialScore: false });
        expect(insertedRows[1]).toMatchObject({ finalPosition: 3, isPartialScore: true });
      }
    });

    it("recalculates standings (calls db.query.seasonSports.findMany)", async () => {
      const { db } = makeDb();
      await processMatchResult({ ...BASE, round: "Quarterfinals", isScoring: true }, db);
      expect((db.query.seasonSports.findMany as ReturnType<typeof vi.fn>)).toHaveBeenCalled();
    });
  });

  describe("Semifinals (scoring)", () => {
    it("loser gets position 3 (final), winner gets position 2 (provisional)", async () => {
      const { db, insertedRows } = makeDb();

      await processMatchResult({ ...BASE, round: "Semifinals", isScoring: true }, db);

      expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 3, isPartialScore: false });
      expect(insertedRows[1]).toMatchObject({ participantId: "winner-1", finalPosition: 2, isPartialScore: true });
    });

    it("also works for Final Four / Conference Finals / Conference Championship", async () => {
      for (const round of ["Final Four", "Conference Finals", "Conference Championship"]) {
        const { db, insertedRows } = makeDb();
        await processMatchResult({ ...BASE, round, isScoring: true }, db);
        expect(insertedRows[0]).toMatchObject({ finalPosition: 3, isPartialScore: false });
        expect(insertedRows[1]).toMatchObject({ finalPosition: 2, isPartialScore: true });
      }
    });
  });

  describe("Finals / Championship (scoring)", () => {
    it("loser=2nd (final), winner=1st (final), no third row", async () => {
      const { db, insertedRows } = makeDb();

      await processMatchResult({ ...BASE, round: "Finals", isScoring: true }, db);

      expect(insertedRows).toHaveLength(2);
      expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 2, isPartialScore: false });
      expect(insertedRows[1]).toMatchObject({ participantId: "winner-1", finalPosition: 1, isPartialScore: false });
    });

    it("handles all Finals aliases the same way", async () => {
      for (const round of ["Championship", "Super Bowl", "NBA Finals", "Grand Final"]) {
        const { db, insertedRows } = makeDb();
        await processMatchResult({ ...BASE, round, isScoring: true }, db);
        expect(insertedRows).toHaveLength(2);
        expect(insertedRows[0]).toMatchObject({ finalPosition: 2, isPartialScore: false });
        expect(insertedRows[1]).toMatchObject({ finalPosition: 1, isPartialScore: false });
      }
    });
  });

  describe("Non-scoring round (isScoring=false)", () => {
    it("loser eliminated (pos 0 final), winner gets provisional T5 floor (no template = legacy)", async () => {
      const { db, insertedRows } = makeDb();

      await processMatchResult({ ...BASE, round: "Round of 16", isScoring: false }, db);

      expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 0, isPartialScore: false });
      expect(insertedRows[1]).toMatchObject({ participantId: "winner-1", finalPosition: 5, isPartialScore: true });
    });

    describe("NCAA 68 — only Sweet Sixteen winners earn a floor", () => {
      const NCAA = { bracketTemplateId: "ncaa_68" };

      it("Round of 64: loser=0, winner gets NO score (not yet in scoring bracket)", async () => {
        const { db, insertedRows } = makeDb();
        await processMatchResult({ ...BASE, ...NCAA, round: "Round of 64", isScoring: false }, db);
        expect(insertedRows).toHaveLength(1);
        expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 0, isPartialScore: false });
      });

      it("Round of 32: loser=0, winner gets NO score (not yet in scoring bracket)", async () => {
        const { db, insertedRows } = makeDb();
        await processMatchResult({ ...BASE, ...NCAA, round: "Round of 32", isScoring: false }, db);
        expect(insertedRows).toHaveLength(1);
        expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 0, isPartialScore: false });
      });

      it("Sweet Sixteen: loser=0, winner gets provisional T5 floor (entering Elite Eight)", async () => {
        const { db, insertedRows } = makeDb();
        await processMatchResult({ ...BASE, ...NCAA, round: "Sweet Sixteen", isScoring: false }, db);
        expect(insertedRows).toHaveLength(2);
        expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 0, isPartialScore: false });
        expect(insertedRows[1]).toMatchObject({ participantId: "winner-1", finalPosition: 5, isPartialScore: true });
      });

      it("First Four: loser=0, winner gets NO score (feeds into Round of 64, not scoring)", async () => {
        const { db, insertedRows } = makeDb();
        await processMatchResult({ ...BASE, ...NCAA, round: "First Four", isScoring: false }, db);
        expect(insertedRows).toHaveLength(1);
        expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 0, isPartialScore: false });
      });
    });

    it("AFL Wildcard Round: loser=0, winner gets T5 floor (feeds into Elimination Finals = scoring)", async () => {
      const { db, insertedRows } = makeDb();
      await processMatchResult({ ...BASE, bracketTemplateId: "afl_10", round: "Wildcard Round", isScoring: false }, db);
      expect(insertedRows).toHaveLength(2);
      expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 0, isPartialScore: false });
      expect(insertedRows[1]).toMatchObject({ participantId: "winner-1", finalPosition: 5, isPartialScore: true });
    });

    describe("NBA Play-In loserAdvances=true (7v8 game)", () => {
      it("loser is NOT eliminated when loserAdvances=true (advances to Round 2)", async () => {
        const { db, insertedRows } = makeDb();
        await processMatchResult(
          {
            ...BASE,
            bracketTemplateId: "nba_20",
            round: "Play-In Round 1",
            isScoring: false,
            loserAdvances: true,
          },
          db
        );
        // Neither participant gets a result: loser skipped (still playing), and
        // winner gets nothing because PIR1 doesn't feed directly into a scoring round.
        expect(insertedRows).toHaveLength(0);
      });

      it("loser IS eliminated when loserAdvances=false (9v10 game)", async () => {
        const { db, insertedRows } = makeDb();
        await processMatchResult(
          {
            ...BASE,
            bracketTemplateId: "nba_20",
            round: "Play-In Round 1",
            isScoring: false,
            loserAdvances: false,
          },
          db
        );
        expect(insertedRows).toHaveLength(1);
        expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 0, isPartialScore: false });
      });
    });
  });

  describe("Unrecognized scoring round", () => {
    it("gives loser 0 pts and still completes without throwing", async () => {
      const { db, insertedRows } = makeDb();

      await processMatchResult({ ...BASE, round: "Mystery Round", isScoring: true }, db);

      expect(insertedRows[0]).toMatchObject({ finalPosition: 0, isPartialScore: false });
    });
  });

  describe("AFL rounds", () => {
    it("Elimination Finals: loser exits at position 7 (final)", async () => {
      const { db, insertedRows } = makeDb();

      await processMatchResult(
        { ...BASE, round: "Elimination Finals", isScoring: true, bracketTemplateId: "afl_10" },
        db
      );

      expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 7, isPartialScore: false });
    });

    it("Qualifying Finals: loser is still alive (provisional floor 5)", async () => {
      const { db, insertedRows } = makeDb();

      await processMatchResult(
        { ...BASE, round: "Qualifying Finals", isScoring: true, bracketTemplateId: "afl_10" },
        db
      );

      // Loser row is provisional (isPartialScore=true)
      expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 5, isPartialScore: true });
    });
  });

  describe("Idempotency: already-final row is not un-finalized", () => {
    it("does not overwrite a finalized row with a partial score", async () => {
      // findFirst returns a finalized row for the first participant (loser)
      const existing = { id: "result-1", isPartialScore: false };
      const { db, updatedRows } = makeDb(existing);

      await processMatchResult(
        { ...BASE, round: "Quarterfinals", isScoring: true },
        db
      );

      // The guard in upsertParticipantResult should skip the update when
      // existing.isPartialScore=false and we're trying to write isPartialScore=true.
      // The loser row would try to write isPartialScore=false (final) — that IS
      // allowed (both false). The winner row tries isPartialScore=true — since
      // there's no existing row for the winner (findFirst returns undefined on
      // second call), it inserts.
      //
      // Key assertion: no update/insert with isPartialScore=true was applied to
      // the existing finalized row.
      const finalizedOverwrite = updatedRows.find(
        (r: any) => r.isPartialScore === true
      );
      expect(finalizedOverwrite).toBeUndefined();
    });
  });

  describe("Probability recalculation", () => {
    it("always calls updateProbabilitiesAfterResult with the sports season id", async () => {
      const { db } = makeDb();

      await processMatchResult({ ...BASE, round: "Quarterfinals", isScoring: true }, db);

      expect(updateProbabilitiesAfterResult).toHaveBeenCalledWith("ss-1", true);
    });

    it("does not throw even if probability update fails", async () => {
      (updateProbabilitiesAfterResult as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
        new Error("network error")
      );
      const { db } = makeDb();

      // Should not throw — the error is caught and logged
      await expect(
        processMatchResult({ ...BASE, round: "Quarterfinals", isScoring: true }, db)
      ).resolves.not.toThrow();
    });
  });
});

describe("doesLoserAdvance", () => {
  it("returns true for NBA Play-In Round 1 match 1 (E7v8)", () => {
    expect(doesLoserAdvance("Play-In Round 1", 1, "nba_20")).toBe(true);
  });

  it("returns true for NBA Play-In Round 1 match 3 (W7v8)", () => {
    expect(doesLoserAdvance("Play-In Round 1", 3, "nba_20")).toBe(true);
  });

  it("returns false for NBA Play-In Round 1 match 2 (E9v10 — loser eliminated)", () => {
    expect(doesLoserAdvance("Play-In Round 1", 2, "nba_20")).toBe(false);
  });

  it("returns false for NBA Play-In Round 1 match 4 (W9v10 — loser eliminated)", () => {
    expect(doesLoserAdvance("Play-In Round 1", 4, "nba_20")).toBe(false);
  });

  it("returns false for NBA Play-In Round 2 (all losers eliminated)", () => {
    expect(doesLoserAdvance("Play-In Round 2", 1, "nba_20")).toBe(false);
    expect(doesLoserAdvance("Play-In Round 2", 2, "nba_20")).toBe(false);
  });

  it("returns false for other templates", () => {
    expect(doesLoserAdvance("Play-In Round 1", 1, "nfl_14")).toBe(false);
    expect(doesLoserAdvance("Semifinals", 1, "fifa_48")).toBe(false);
  });

  it("returns false for an empty templateId", () => {
    expect(doesLoserAdvance("Play-In Round 1", 1, "")).toBe(false);
  });
});

describe("isLoserNotifiable", () => {
  const LOSER = "loser-1";
  const TEAM = "team-1";

  it("returns true when the loser's team score changed", () => {
    expect(
      isLoserNotifiable(LOSER, TEAM, new Set([TEAM]), new Set())
    ).toBe(true);
  });

  it("returns true when the loser is finalized even with no score change (0-pt elimination)", () => {
    expect(
      isLoserNotifiable(LOSER, TEAM, new Set(), new Set([LOSER]))
    ).toBe(true);
  });

  it("returns false when score did not change and loser is not finalized (advances)", () => {
    expect(
      isLoserNotifiable(LOSER, TEAM, new Set(), new Set())
    ).toBe(false);
  });

  it("returns false when loserId is null", () => {
    expect(
      isLoserNotifiable(null, TEAM, new Set([TEAM]), new Set())
    ).toBe(false);
  });

  it("returns false when loserId is undefined", () => {
    expect(
      isLoserNotifiable(undefined, undefined, new Set(), new Set())
    ).toBe(false);
  });

  it("returns false when loser has no team (undrafted) and is not finalized", () => {
    expect(
      isLoserNotifiable(LOSER, undefined, new Set(), new Set())
    ).toBe(false);
  });

  it("returns true when loser has no team but is finalized (edge case: drafted mid-season?)", () => {
    // finalizedLoserIds wins regardless of team presence
    expect(
      isLoserNotifiable(LOSER, undefined, new Set(), new Set([LOSER]))
    ).toBe(true);
  });
});

describe("processPlayoffEvent - NBA Play-In Round 1 loserAdvances fix", () => {
  function makePlayInDb(matches: object[]) {
    const insertedRows: Array<{ participantId: string; finalPosition: number; isPartialScore: boolean }> = [];
    const insertValues = vi.fn().mockImplementation((values: object) => {
      insertedRows.push(values as typeof insertedRows[0]);
      return Promise.resolve();
    });

    return {
      db: {
        insert: vi.fn().mockReturnValue({ values: insertValues }),
        update: vi.fn().mockReturnValue({
          set: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(undefined) }),
        }),
        delete: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(undefined) }),
        query: {
          scoringEvents: {
            findFirst: vi.fn().mockResolvedValue({
              id: "event-1",
              playoffRound: "Play-In Round 1",
              bracketTemplateId: "nba_20",
              sportsSeasonId: "ss-1",
              name: "NBA Playoffs",
              sportsSeason: {
                seasonSports: [{ seasonId: "season-1", season: {} }],
              },
            }),
          },
          playoffMatches: {
            findMany: vi.fn().mockResolvedValue(matches),
          },
          seasonParticipants: {
            findMany: vi.fn().mockResolvedValue([]),
          },
          seasonParticipantResults: {
            findFirst: vi.fn().mockResolvedValue(undefined),
          },
          seasons: {
            findFirst: vi.fn().mockResolvedValue({
              id: "season-1",
              pointsFor1st: 100, pointsFor2nd: 70, pointsFor3rd: 50,
              pointsFor4th: 40, pointsFor5th: 25, pointsFor6th: 25,
              pointsFor7th: 15, pointsFor8th: 15,
            }),
          },
          seasonSports: { findMany: vi.fn().mockResolvedValue([]) },
          sportsSeasons: { findFirst: vi.fn().mockResolvedValue(null) },
        },
      } as any,
      insertedRows,
    };
  }

  // All four Play-In Round 1 matches: East 7v8 (M1), East 9v10 (M2), West 7v8 (M3), West 9v10 (M4)
  const PLAY_IN_MATCHES = [
    {
      id: "m1", round: "Play-In Round 1", matchNumber: 1, isScoring: false,
      isComplete: true, scoringEventId: "event-1",
      participant1Id: "winner-78e", participant2Id: "loser-78e",
      winnerId: "winner-78e", loserId: "loser-78e",
    },
    {
      id: "m2", round: "Play-In Round 1", matchNumber: 2, isScoring: false,
      isComplete: true, scoringEventId: "event-1",
      participant1Id: "winner-910e", participant2Id: "loser-910e",
      winnerId: "winner-910e", loserId: "loser-910e",
    },
    {
      id: "m3", round: "Play-In Round 1", matchNumber: 3, isScoring: false,
      isComplete: true, scoringEventId: "event-1",
      participant1Id: "winner-78w", participant2Id: "loser-78w",
      winnerId: "winner-78w", loserId: "loser-78w",
    },
    {
      id: "m4", round: "Play-In Round 1", matchNumber: 4, isScoring: false,
      isComplete: true, scoringEventId: "event-1",
      participant1Id: "winner-910w", participant2Id: "loser-910w",
      winnerId: "winner-910w", loserId: "loser-910w",
    },
  ];

  it("eliminates 9v10 losers (East & West) but NOT 7v8 losers", async () => {
    const { db, insertedRows } = makePlayInDb(PLAY_IN_MATCHES);

    await processPlayoffEvent("event-1", db, { skipRecalculate: true });

    const eliminatedIds = insertedRows
      .filter((r) => r.finalPosition === 0 && !r.isPartialScore)
      .map((r) => r.participantId);

    // 7v8 losers advance to Play-In Round 2 — must not be eliminated yet
    expect(eliminatedIds).not.toContain("loser-78e");
    expect(eliminatedIds).not.toContain("loser-78w");
    // 9v10 losers are out immediately
    expect(eliminatedIds).toContain("loser-910e");
    expect(eliminatedIds).toContain("loser-910w");
  });

  it("eliminates ALL losers when bracketTemplateId is missing (falls back to safe default)", async () => {
    // Guards against a future refactor that drops bracketTemplateId from the DB query.
    // doesLoserAdvance receives "" → returns false for all matches → all losers eliminated.
    const { db, insertedRows } = makePlayInDb(PLAY_IN_MATCHES);
    (db.query.scoringEvents.findFirst as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
      id: "event-1",
      playoffRound: "Play-In Round 1",
      bracketTemplateId: null, // simulates missing templateId
      sportsSeasonId: "ss-1",
      name: "NBA Playoffs",
      sportsSeason: {
        seasonSports: [{ seasonId: "season-1", season: {} }],
      },
    });

    await processPlayoffEvent("event-1", db, { skipRecalculate: true });

    const eliminatedIds = insertedRows
      .filter((r) => r.finalPosition === 0 && !r.isPartialScore)
      .map((r) => r.participantId);

    expect(eliminatedIds).toContain("loser-78e");
    expect(eliminatedIds).toContain("loser-78w");
  });
});
