import { describe, expect, it } from "vitest"

import { Results, RoundResults, Score, TournamentType } from "../results.js"
import { Tiebreak, Tiebreaker, UnplayedRoundsAdjustment } from "../tiebreak.js"
import { readTestCases } from "./util/test-case-reader.js"

describe("TiebreakCalculation", () => {
  describe("score", () => {
    it("should sum score of all pairings", () => {
      const results = new Results(TournamentType.SWISS, [
        round(["A:B 1:0"]),
        round(["C:A 0:1 forfeit"]),
        round(["C:B 0.5:0.5"]),
        round(["B:A 1:0 forfeit"]),
      ])

      const tiebreak = new Tiebreaker(results, UnplayedRoundsAdjustment.NONE)
      expect(tiebreak.score("A", 1)).toEqual(1)
      expect(tiebreak.score("A", 2)).toEqual(2)
      expect(tiebreak.score("A", 3)).toEqual(2)
      expect(tiebreak.score("A", 4)).toEqual(2)
      expect(tiebreak.score("B", 1)).toEqual(0)
      expect(tiebreak.score("B", 2)).toEqual(0)
      expect(tiebreak.score("B", 3)).toEqual(0.5)
      expect(tiebreak.score("B", 4)).toEqual(1.5)
      expect(tiebreak.score("C", 1)).toEqual(0)
      expect(tiebreak.score("C", 2)).toEqual(0)
      expect(tiebreak.score("C", 3)).toEqual(0.5)
      expect(tiebreak.score("C", 4)).toEqual(0.5)
    })

    it("should score byes correctly", () => {
      const tiebreak = new Tiebreaker(
        new Results(TournamentType.SWISS, [
          { pairings: [], pairingAllocatedByes: ["A"] },
          { pairings: [], halfPointByes: ["A"] },
        ]),
        UnplayedRoundsAdjustment.NONE,
      )

      expect(tiebreak.score("A", 1)).toEqual(1)
      expect(tiebreak.score("A", 2)).toEqual(1.5)
      expect(tiebreak.score("A", 3)).toEqual(1.5)
    })
  })

  describe("buchholz", () => {
    describe("without unplayed rounds adjustment", () => {
      // TODO: test byes
      it("should sum points of opponents", () => {
        const rounds = new Results(TournamentType.SWISS, [
          round(["A:B 1:0"]),
          round(["C:A 0.5:0.5"]),
          round(["B:A 0:1 forfeit"]),
        ])
        const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.NONE)

        expect(tiebreak.buchholz("A", 1)).toEqual(0)
        expect(tiebreak.buchholz("B", 1)).toEqual(1)
        expect(tiebreak.buchholz("C", 1)).toEqual(0)
        expect(tiebreak.buchholz("A", 2)).toEqual(0 + 0.5)
        expect(tiebreak.buchholz("B", 2)).toEqual(1.5)
        expect(tiebreak.buchholz("C", 2)).toEqual(1.5)
        expect(tiebreak.buchholz("A", 3)).toEqual(0 + 0.5)
        expect(tiebreak.buchholz("B", 3)).toEqual(2 * 2.5)
        expect(tiebreak.buchholz("C", 3)).toEqual(2.5)
      })

      it("should pass FIDE exercise 1 (2023)", async () => {
        const rounds = await readTestCases("fide-exercise-2023")
        const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.NONE)
        expect(tiebreak.buchholz("2", 5)).toEqual(13)
      })

      it("should pass FIDE exercise 2 (2023)", async () => {
        const rounds = await readTestCases("fide-exercise-2023")
        const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.NONE)
        expect(tiebreak.buchholz("1", 5)).toEqual(12.5)
        expect(tiebreak.buchholz("3", 5)).toEqual(15.5)
      })

      it("should NOT pass FIDE exercise 3 (2023)", async () => {
        const rounds = await readTestCases("fide-exercise-2023")
        const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.NONE)
        expect(tiebreak.buchholz("5", 5)).toEqual(8.5)
        expect(tiebreak.buchholz("8", 5)).toEqual(13.5)
        expect(tiebreak.buchholz("11", 5)).toEqual(12.5) // 13.5 with FIDE_2023
      })
    })

    describe("with FIDE_2023 unplayed rounds adjustment", () => {
      it("should pass FIDE exercise 1 (2023)", async () => {
        const rounds = await readTestCases("fide-exercise-2023")
        const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.FIDE_2023)
        expect(tiebreak.buchholz("2", 5)).toEqual(13)
      })

      it("should pass FIDE exercise 2 (2023)", async () => {
        const rounds = await readTestCases("fide-exercise-2023")
        const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.FIDE_2023)
        expect(tiebreak.buchholz("1", 5)).toEqual(12.5)
        expect(tiebreak.buchholz("3", 5)).toEqual(15.5)
      })

      it("should pass FIDE exercise 3 (2023)", async () => {
        const rounds = await readTestCases("fide-exercise-2023")
        const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.FIDE_2023)
        expect(tiebreak.buchholz("5", 5)).toEqual(8.5)
        expect(tiebreak.buchholz("8", 5)).toEqual(13.5)
        expect(tiebreak.buchholz("11", 5)).toEqual(13.5)
      })

      it("should pass FIDE exercise 4 (2023)", async () => {
        const rounds = await readTestCases("fide-exercise-2023")
        const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.FIDE_2023)
        expect(tiebreak.buchholz("1", 5)).toEqual(12.5)
        expect(tiebreak.buchholz("3", 5)).toEqual(15.5)
        expect(tiebreak.buchholz("16", 5)).toEqual(12.5)
        expect(tiebreak.buchholz("4", 5)).toEqual(15.0)
      })

      it("should pass FIDE exercise 5 (2023)", async () => {
        const rounds = await readTestCases("fide-exercise-2023")
        const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.FIDE_2023)
        expect(tiebreak.buchholz("5", 5, 1)).toEqual(7.5)
        expect(tiebreak.buchholz("8", 5, 1)).toEqual(12)
        expect(tiebreak.buchholz("11", 5, 1)).toEqual(12)
      })

      it("should pass FIDE exercise 6 (2023)", async () => {
        const rounds = await readTestCases("fide-exercise-2023")
        const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.FIDE_2023)
        expect(tiebreak.buchholz("7", 5, 1)).toEqual(12.5)
        expect(tiebreak.buchholz("9", 5, 1)).toEqual(7.5)
        expect(tiebreak.buchholz("13", 5, 1)).toEqual(12)
      })

      it("should pass FIDE exercise 7 (2023)", async () => {
        const rounds = await readTestCases("fide-exercise-2023")
        const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.FIDE_2023)
        expect(tiebreak.buchholz("1", 5, 1)).toEqual(11.0)
        expect(tiebreak.buchholz("3", 5, 1)).toEqual(13)
        expect(tiebreak.buchholz("4", 5, 1)).toEqual(11.5)
        expect(tiebreak.buchholz("16", 5, 1)).toEqual(11)
      })

      it("should pass FIDE exercise 8 (2023)", async () => {
        const rounds = await readTestCases("fide-exercise-2023")
        const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.FIDE_2023)
        expect(tiebreak.buchholz("12", 5, 1)).toEqual(9.5)
        expect(tiebreak.buchholz("14", 5, 1)).toEqual(9)
        expect(tiebreak.buchholz("15", 5, 1)).toEqual(11)
      })
    })

    describe("with FIDE_2009 unplayed rounds adjustment", () => {
      // TODO: byes
      it("should count any unplayed games of opponents as draw", () => {
        const rounds = new Results(TournamentType.SWISS, [
          round(["A:B 1:0", "C:Cx 1:0", "D:Dx 1:0", "E:Ex 1:0"]),
          round(["B:X 1:0 forfeit", "A:C 1:0", "D:Dy 1:0", "E:Ey 1:0"]),
          round(["A:D 1:0", "C:Cy 1:0", "E:Ez 1:0"]),
          round(["B:Y 0:1 forfeit", "A:E 1:0", "C:Cz 1:0", "D:Dz 1:0"]),
        ])
        const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.FIDE_2009)

        expect(tiebreak.buchholz("A", 1)).toEqual(0)
        expect(tiebreak.buchholz("A", 2)).toEqual(1 * 0.5 + 1 * 1)
        expect(tiebreak.buchholz("A", 3)).toEqual(2 * 0.5 + 2 * 2)
        expect(tiebreak.buchholz("A", 4)).toEqual(3 * 0.5 + 3 * 3)
      })

      it("should use virtual opponents for unplayed games", () => {
        const rounds = new Results(TournamentType.SWISS, [
          round(["A:B 1:0"]),
          round(["A:B 1:0 forfeit"]),
        ])
        const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.FIDE_2009)

        // A's first opponent contributes (0 + 0.5) and the second (1 + 0)
        expect(tiebreak.buchholz("A", 2)).toEqual(0.5 + 1)
        // B's first opponent contributes (1 + 0.5) and the second (0 + 1)
        expect(tiebreak.buchholz("B", 2)).toEqual(1.5 + 1)

        // Round 3 (neither A nor B paired)
        expect(tiebreak.buchholz("A", 3)).toEqual(0 + 2 * 0.5 + (1 + 0 + 0.5) + (2 + 1))
        expect(tiebreak.buchholz("B", 3)).toEqual(1 + 2 * 0.5 + (0 + 1 + 0.5) + (0 + 1))
      })
    })

    /*
      TODO: Test more modifiers with old FIDE unplayed rounds adjustments
      - Cut-1
      - Median-1
      - Cut-2
      - Very high cut numbers

      describe("with modifiers", () => {

      })
    */
  })

  describe("sonnebornBerger", () => {
    describe("with FIDE_2023 unplayed rounds adjustment", () => {
      it("should pass FIDE exercise 11 (2023)", async () => {
        const rounds = await readTestCases("fide-exercise-2023")
        const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.FIDE_2023)
        expect(tiebreak.sonnebornBerger("1", 5)).toEqual(8)
        expect(tiebreak.sonnebornBerger("3", 5)).toEqual(10.5)
        expect(tiebreak.sonnebornBerger("16", 5)).toEqual(7.25)
        expect(tiebreak.sonnebornBerger("4", 5)).toEqual(9.75)
      })

      it("should pass FIDE exercise 12 (2023)", async () => {
        const rounds = await readTestCases("fide-exercise-2023")
        const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.FIDE_2023)
        expect(tiebreak.adjustedScore("2", 5)).toEqual(4)
        expect(tiebreak.sonnebornBerger("2", 5)).toEqual(9.5)
        expect(tiebreak.adjustedScore("3", 5)).toEqual(3.5)
        expect(tiebreak.sonnebornBerger("3", 5)).toEqual(10.5)
        expect(tiebreak.adjustedScore("4", 5)).toEqual(3.5)
        expect(tiebreak.sonnebornBerger("4", 5)).toEqual(9.75)
        expect(tiebreak.adjustedScore("1", 5)).toEqual(3.5)
        expect(tiebreak.sonnebornBerger("1", 5)).toEqual(8)
        expect(tiebreak.adjustedScore("16", 5)).toEqual(3.5)
        expect(tiebreak.sonnebornBerger("16", 5)).toEqual(7.25)
        expect(tiebreak.adjustedScore("6", 5)).toEqual(3)
        expect(tiebreak.sonnebornBerger("6", 5)).toEqual(6.5)
        expect(tiebreak.adjustedScore("11", 5)).toEqual(2.5)
        expect(tiebreak.sonnebornBerger("11", 5)).toEqual(5.75)
        expect(tiebreak.adjustedScore("8", 5)).toEqual(2.5)
        expect(tiebreak.sonnebornBerger("8", 5)).toEqual(5.25)
        expect(tiebreak.adjustedScore("5", 5)).toEqual(2.5)
        expect(tiebreak.sonnebornBerger("5", 5)).toEqual(4.25)
        expect(tiebreak.adjustedScore("14", 5)).toEqual(2)
        expect(tiebreak.sonnebornBerger("14", 5)).toEqual(4.5)
        expect(tiebreak.adjustedScore("12", 5)).toEqual(3.0)
        expect(tiebreak.sonnebornBerger("12", 5)).toEqual(4.0)
        expect(tiebreak.adjustedScore("15", 5)).toEqual(2)
        expect(tiebreak.sonnebornBerger("15", 5)).toEqual(3.5)
        expect(tiebreak.adjustedScore("13", 5)).toEqual(1.5)
        expect(tiebreak.sonnebornBerger("13", 5)).toEqual(4.25)
        expect(tiebreak.adjustedScore("7", 5)).toEqual(1.5)
        expect(tiebreak.sonnebornBerger("7", 5)).toEqual(3.25)
        expect(tiebreak.adjustedScore("10", 5)).toEqual(1.0)
        expect(tiebreak.sonnebornBerger("10", 5)).toEqual(1.5)
      })
    })

    // TODO: Test with FIDE_2009 and NONE unplayed rounds adjustments.
  })

  describe("ranking", () => {
    it("should rank simple tournament", () => {
      const results = new Results(TournamentType.SWISS, [
        {
          // Round 1: Player D did not show up
          pairings: [
            { white: "C", black: "A", scoreWhite: 0.5, scoreBlack: 0.5, forfeited: false },
            { white: "B", black: "D", scoreWhite: 1, scoreBlack: 0, forfeited: true },
          ],
        },
        {
          // Round 2: Player D was not paired
          pairings: [{ white: "A", black: "B", scoreWhite: 0, scoreBlack: 1, forfeited: false }],
          pairingAllocatedByes: ["C"],
        },
      ])
      const tiebreak = new Tiebreaker(results, UnplayedRoundsAdjustment.FIDE_2023)
      expect(
        tiebreak.ranking(2, [
          Tiebreak.SCORE,
          Tiebreak.BUCHHOLZ,
          Tiebreak.BUCHHOLZ_CUT1,
          Tiebreak.SONNEBORN_BERGER,
        ]),
      ).toEqual([
        { playerId: "B", rank: 1, scores: [2, 2.5, 2, 2.5] }, // BH: 2 for unplayed + 0.5 for A
        { playerId: "C", rank: 2, scores: [1.5, 2, 1.5, 1.75] }, // BH: 0.5 for A + 1.5 for bye
        { playerId: "A", rank: 3, scores: [0.5, 3.5, 2, 0.75] }, // BH: 1.5 for C + 2 for B
        { playerId: "D", rank: 4, scores: [0, 0, 0, 0] }, // BH: 2 * 0 for unplayed rounds
      ])
    })

    it("should return tied players on the same rank", () => {
      const results = new Results(TournamentType.SWISS, [
        {
          // Round 1: Player D did not show up
          pairings: [
            { white: "C", black: "A", scoreWhite: 0.5, scoreBlack: 0.5, forfeited: false },
            { white: "B", black: "D", scoreWhite: 1, scoreBlack: 0, forfeited: false },
          ],
        },
      ])
      const tiebreak = new Tiebreaker(results, UnplayedRoundsAdjustment.FIDE_2023)
      expect(tiebreak.ranking(1, [Tiebreak.SCORE, Tiebreak.BUCHHOLZ])).toEqual([
        { playerId: "B", rank: 1, scores: [1, 0] },
        { playerId: "A", rank: 2, scores: [0.5, 0.5] },
        { playerId: "C", rank: 2, scores: [0.5, 0.5] },
        { playerId: "D", rank: 4, scores: [0, 1] },
      ])
    })
  })
})

/**
 * Helper for specifying a round results in a more readable format, e.g. "A B 1 0".
 */
// TODO: Use something like Results.fromStrings()
function round(pairings: string[]): RoundResults {
  return {
    pairings: pairings.map((pairing) => {
      const [white, black, scoreWhite, scoreBlack, forfeited] = pairing
        .replaceAll(":", " ")
        .split(" ")
      return {
        white,
        black,
        scoreWhite: parseFloat(scoreWhite) as Score,
        scoreBlack: parseFloat(scoreBlack) as Score,
        forfeited: !!forfeited,
      }
    }),
  }
}
