local ClientMatch = require("client.src.ClientMatch")
local GameModes = require("common.data.GameModes")
local Player = require("client.src.Player")
local consts = require("common.engine.consts")
local GeneratorSource = require("common.engine.GeneratorSource")
local logger = require("common.lib.logger")
local LevelPresets = require("common.data.LevelPresets")

local Theme = require("client.src.mods.Theme")

local function test(func)
  func()
end

local legacyScoreX = 546
local legacyScoreXP2 = 642
local legacyScoreY = 208

---@param playerCount integer
---@param theme table?
---@return ClientMatch
local function createEndlessClientMatch(playerCount, theme)
  local endless = GameModes.getPreset("ONE_PLAYER_ENDLESS")
  local players = {}
  if playerCount == nil then
    playerCount = 1
  end
  for i = 1, playerCount do
    local player = Player.getLocalPlayer()
    player.isLocal = false
    player:setLevel(10)
    player:setLevelData(LevelPresets.getModern(10))
    player:setStyle(GameModes.Styles.MODERN)
    player.playerNumber = i

    --player:restrictInputs(inputs.inputConfigurations[i])
    players[#players+1] = player
  end

  local clientMatch = ClientMatch.createFromGameMode(players, endless, GeneratorSource(math.random(1, 999999), true), false)
  clientMatch:start()

  if theme then
    for i, stack in ipairs(clientMatch.stacks) do
      stack.theme = theme
      -- different theme changes the offsets so we need to recalculate graphics values
      stack:moveForRenderIndex(i)
    end
  end

  return clientMatch
end

local v1Theme = Theme("client/tests/ThemeTestData/V1Test", "V1Test")
assert(v1Theme ~= nil)
local defaultTheme = Theme(Theme.defaultThemeDirectoryPath, consts.DEFAULT_THEME_DIRECTORY)
assert(defaultTheme ~= nil)

-- ORIGIN TESTING

local function testOriginalThemeStackGraphics()
  local match = createEndlessClientMatch(1, v1Theme)
  local stack = match.stacks[1]

  assert(match ~= nil)
  assert(stack.origin_x == 184)
  assert(stack:elementOriginX(false, false) == stack.origin_x * stack.gfxScale)
  assert(stack:elementOriginY(false, false) == stack.panelOriginY * stack.gfxScale)
  assert(stack:elementOriginX(true, false) == legacyScoreX)
  assert(stack:elementOriginY(true, false) == legacyScoreY)
end

--test(testOriginalThemeStackGraphics)

local function testOriginalThemeStackGraphicsPlayer2()
  local match = createEndlessClientMatch(2, v1Theme)
  local stack = match.stacks[2]

  assert(match ~= nil)
  assert(stack.origin_x * stack.gfxScale == 728)
  assert(stack:elementOriginX(false, false) == stack.origin_x * stack.gfxScale)
  assert(stack:elementOriginY(false, false) == stack.panelOriginY * stack.gfxScale)
  assert(stack:elementOriginX(true, false) == legacyScoreXP2)
  assert(stack:elementOriginY(true, false) == legacyScoreY)
end

--test(testOriginalThemeStackGraphicsPlayer2)

local function testNewThemeStackGraphics()
  local match = createEndlessClientMatch(1, defaultTheme)
  local stack = match.stacks[1]

  assert(match ~= nil)
  -- disabled because we no longer use origin x to draw that in modern theme versions
  --assert(stack.origin_x == 184)
  assert(stack:elementOriginX(false, false) == stack.origin_x * stack.gfxScale)
  assert(stack:elementOriginY(false, false) == stack.panelOriginY * stack.gfxScale)
  assert(stack:elementOriginX(true, false) == stack.origin_x * stack.gfxScale)
  assert(stack:elementOriginY(true, false) == stack.panelOriginY * stack.gfxScale)
end

test(testNewThemeStackGraphics)

local function testNewThemeStackGraphicsPlayer2()
  local match = createEndlessClientMatch(2, defaultTheme)
  local stack = match.stacks[2]

  assert(match ~= nil)
  -- disabled because we no longer use origin x to draw that in modern theme versions
  --assert(stack.origin_x * stack.gfxScale == 728)
  assert(stack:elementOriginX(false, false) == stack.origin_x * stack.gfxScale)
  assert(stack:elementOriginY(false, false) == stack.panelOriginY * stack.gfxScale)
  assert(stack:elementOriginX(true, false) == stack.origin_x * stack.gfxScale)
  assert(stack:elementOriginY(true, false) == stack.panelOriginY * stack.gfxScale)
end

test(testNewThemeStackGraphicsPlayer2)

-- ORIGIN SHIFT TESTING 

local function testOriginalThemeOffset()
  local match = createEndlessClientMatch(1, v1Theme)
  local stack = match.stacks[1]

  assert(match ~= nil)
  -- non "score" based values WERE in stack.gfxScale coordinates
  assert(stack:elementOriginXWithOffset({100, 100}, false) == stack.origin_x * stack.gfxScale + 100 * stack.gfxScale)
  assert(stack:elementOriginYWithOffset({100, 100}, false) == stack.panelOriginY * stack.gfxScale + 100 * stack.gfxScale)
  -- legacy score offsets were in absolute coordinates
  assert(stack:elementOriginXWithOffset({100, 100}, true) == legacyScoreX + 100)
  assert(stack:elementOriginYWithOffset({100, 100}, true) == legacyScoreY + 100)

  -- player 1 doesn't offset by width
  assert(stack:labelOriginXWithOffset({100, 100}, 1, false, 100, 0) == stack.origin_x * stack.gfxScale + 100 * stack.gfxScale - 0)
  assert(stack:labelOriginXWithOffset({100, 100}, 1, true, 100, 0) == legacyScoreX + 100 - 0)
end

--test(testOriginalThemeOffset)

local function testOriginalThemeOffsetPlayer2()
  local match = createEndlessClientMatch(2, v1Theme)
  local stack = match.stacks[2]

  assert(match ~= nil)
  -- non "score" based values WERE in stack.gfxScale coordinates, and mirror
  assert(stack:elementOriginXWithOffset({100, 100}, false) == stack.origin_x * stack.gfxScale - 100 * stack.gfxScale)
  assert(stack:elementOriginYWithOffset({100, 100}, false) == stack.panelOriginY * stack.gfxScale + 100 * stack.gfxScale)
  -- legacy score offsets were in absolute coordinates, and did NOT mirror
  assert(stack:elementOriginXWithOffset({100, 100}, true) == legacyScoreXP2 + 100)
  assert(stack:elementOriginYWithOffset({100, 100}, true) == legacyScoreY + 100)

  -- player 2 offsets by width in absolute coordinates
  assert(stack:labelOriginXWithOffset({100, 100}, 1, false, 100, 1) == stack.origin_x * stack.gfxScale - 100 * stack.gfxScale - 100)
  assert(stack:labelOriginXWithOffset({100, 100}, 1, true, 100, 1) == legacyScoreXP2 + 100 - 100)
end

--test(testOriginalThemeOffsetPlayer2)

local function testNewThemeOffset()
  local match = createEndlessClientMatch(1, defaultTheme)
  local stack = match.stacks[1]

  assert(match ~= nil)
  -- non "score" based values in new themes are in absolute coordinates
  assert(stack:elementOriginXWithOffset({100, 100}, false) == stack.origin_x * stack.gfxScale + 100)
  assert(stack:elementOriginYWithOffset({100, 100}, false) == stack.panelOriginY * stack.gfxScale + 100)
  -- legacy score offsets in the new themes are the same as above, absolute coordinates from the origin_x / panelOriginY
  assert(stack:elementOriginXWithOffset({100, 100}, true) == stack.origin_x * stack.gfxScale + 100)
  assert(stack:elementOriginYWithOffset({100, 100}, true) == stack.panelOriginY * stack.gfxScale + 100)

  -- player 1 doesn't offset by width
  assert(stack:labelOriginXWithOffset({100, 100}, 1, false, 100, 0) == stack.origin_x * stack.gfxScale + 100 - 0)
  assert(stack:labelOriginXWithOffset({100, 100}, 1, true, 100, 0) == stack.origin_x * stack.gfxScale + 100 - 0)
end

test(testNewThemeOffset)

local function testNewThemeOffsetPlayer2()
  local match = createEndlessClientMatch(2, defaultTheme)
  local stack = match.stacks[2]

  assert(match ~= nil)
  -- non "score" based values in new themes are in absolute coordinates
  assert(stack:elementOriginXWithOffset({100, 100}, false) == stack.origin_x * stack.gfxScale - 100)
  assert(stack:elementOriginYWithOffset({100, 100}, false) == stack.panelOriginY * stack.gfxScale + 100)
  -- legacy score offsets in the new themes are the same as above, absolute coordinates from the origin_x / panelOriginY
  assert(stack:elementOriginXWithOffset({100, 100}, true) == stack.origin_x * stack.gfxScale - 100)
  assert(stack:elementOriginYWithOffset({100, 100}, true) == stack.panelOriginY * stack.gfxScale + 100)

  -- player 2 offsets by width in absolute coordinates
  assert(stack:labelOriginXWithOffset({100, 100}, 1, false, 100, 1) == stack.origin_x * stack.gfxScale - 100 - 100)
  assert(stack:labelOriginXWithOffset({100, 100}, 1, true, 100, 1) == stack.origin_x * stack.gfxScale - 100 - 100)
end

test(testNewThemeOffsetPlayer2)

local function testShakeOffsetLargeGarbage()
  local match = createEndlessClientMatch(2, defaultTheme)
  local stack = match.stacks[2]
  ---@cast stack PlayerStack

  -- for i = 76, 1, -1 do
  --   logger.info("assert(stack:shakeOffsetForShakeFrames(" .. i .. ", 0, 1) == " .. stack:shakeOffsetForShakeFrames(i, 0, 1) .. ")")
  -- end

  -- This is a characterization test, I.E. I just recorded the current behavior so I could prove I didn't break it.
  -- Thus I can't vouch that these values are "expected" just that I didn't change them.
  assert(stack:shakeOffsetForShakeFrames(76, 0, 1) == 2)
  assert(stack:shakeOffsetForShakeFrames(75, 0, 1) == 0)
  assert(stack:shakeOffsetForShakeFrames(74, 0, 1) == 3)
  assert(stack:shakeOffsetForShakeFrames(73, 0, 1) == 8)
  assert(stack:shakeOffsetForShakeFrames(72, 0, 1) == 16)
  assert(stack:shakeOffsetForShakeFrames(71, 0, 1) == 23)
  assert(stack:shakeOffsetForShakeFrames(70, 0, 1) == 30)
  assert(stack:shakeOffsetForShakeFrames(69, 0, 1) == 33)
  assert(stack:shakeOffsetForShakeFrames(68, 0, 1) == 32)
  assert(stack:shakeOffsetForShakeFrames(67, 0, 1) == 28)
  assert(stack:shakeOffsetForShakeFrames(66, 0, 1) == 22)
  assert(stack:shakeOffsetForShakeFrames(65, 0, 1) == 14)
  assert(stack:shakeOffsetForShakeFrames(64, 0, 1) == 7)
  assert(stack:shakeOffsetForShakeFrames(63, 0, 1) == 2)
  assert(stack:shakeOffsetForShakeFrames(62, 0, 1) == 0)
  assert(stack:shakeOffsetForShakeFrames(61, 0, 1) == 2)
  assert(stack:shakeOffsetForShakeFrames(60, 0, 1) == 8)
  assert(stack:shakeOffsetForShakeFrames(59, 0, 1) == 15)
  assert(stack:shakeOffsetForShakeFrames(58, 0, 1) == 21)
  assert(stack:shakeOffsetForShakeFrames(57, 0, 1) == 26)
  assert(stack:shakeOffsetForShakeFrames(56, 0, 1) == 27)
  assert(stack:shakeOffsetForShakeFrames(55, 0, 1) == 25)
  assert(stack:shakeOffsetForShakeFrames(54, 0, 1) == 20)
  assert(stack:shakeOffsetForShakeFrames(53, 0, 1) == 13)
  assert(stack:shakeOffsetForShakeFrames(52, 0, 1) == 7)
  assert(stack:shakeOffsetForShakeFrames(51, 0, 1) == 2)
  assert(stack:shakeOffsetForShakeFrames(50, 0, 1) == 0)
  assert(stack:shakeOffsetForShakeFrames(49, 0, 1) == 2)
  assert(stack:shakeOffsetForShakeFrames(48, 0, 1) == 7)
  assert(stack:shakeOffsetForShakeFrames(47, 0, 1) == 13)
  assert(stack:shakeOffsetForShakeFrames(46, 0, 1) == 19)
  assert(stack:shakeOffsetForShakeFrames(45, 0, 1) == 21)
  assert(stack:shakeOffsetForShakeFrames(44, 0, 1) == 21)
  assert(stack:shakeOffsetForShakeFrames(43, 0, 1) == 17)
  assert(stack:shakeOffsetForShakeFrames(42, 0, 1) == 12)
  assert(stack:shakeOffsetForShakeFrames(41, 0, 1) == 6)
  assert(stack:shakeOffsetForShakeFrames(40, 0, 1) == 2)
  assert(stack:shakeOffsetForShakeFrames(39, 0, 1) == 0)
  assert(stack:shakeOffsetForShakeFrames(38, 0, 1) == 2)
  assert(stack:shakeOffsetForShakeFrames(37, 0, 1) == 7)
  assert(stack:shakeOffsetForShakeFrames(36, 0, 1) == 12)
  assert(stack:shakeOffsetForShakeFrames(35, 0, 1) == 16)
  assert(stack:shakeOffsetForShakeFrames(34, 0, 1) == 17)
  assert(stack:shakeOffsetForShakeFrames(33, 0, 1) == 15)
  assert(stack:shakeOffsetForShakeFrames(32, 0, 1) == 10)
  assert(stack:shakeOffsetForShakeFrames(31, 0, 1) == 6)
  assert(stack:shakeOffsetForShakeFrames(30, 0, 1) == 2)
  assert(stack:shakeOffsetForShakeFrames(29, 0, 1) == 0)
  assert(stack:shakeOffsetForShakeFrames(28, 0, 1) == 2)
  assert(stack:shakeOffsetForShakeFrames(27, 0, 1) == 6)
  assert(stack:shakeOffsetForShakeFrames(26, 0, 1) == 10)
  assert(stack:shakeOffsetForShakeFrames(25, 0, 1) == 12)
  assert(stack:shakeOffsetForShakeFrames(24, 0, 1) == 12)
  assert(stack:shakeOffsetForShakeFrames(23, 0, 1) == 9)
  assert(stack:shakeOffsetForShakeFrames(22, 0, 1) == 5)
  assert(stack:shakeOffsetForShakeFrames(21, 0, 1) == 2)
  assert(stack:shakeOffsetForShakeFrames(20, 0, 1) == 0)
  assert(stack:shakeOffsetForShakeFrames(19, 0, 1) == 2)
  assert(stack:shakeOffsetForShakeFrames(18, 0, 1) == 5)
  assert(stack:shakeOffsetForShakeFrames(17, 0, 1) == 7)
  assert(stack:shakeOffsetForShakeFrames(16, 0, 1) == 8)
  assert(stack:shakeOffsetForShakeFrames(15, 0, 1) == 7)
  assert(stack:shakeOffsetForShakeFrames(14, 0, 1) == 4)
  assert(stack:shakeOffsetForShakeFrames(13, 0, 1) == 1)
  assert(stack:shakeOffsetForShakeFrames(12, 0, 1) == 0)
  assert(stack:shakeOffsetForShakeFrames(11, 0, 1) == 1)
  assert(stack:shakeOffsetForShakeFrames(10, 0, 1) == 3)
  assert(stack:shakeOffsetForShakeFrames(9, 0, 1) == 5)
  assert(stack:shakeOffsetForShakeFrames(8, 0, 1) == 4)
  assert(stack:shakeOffsetForShakeFrames(7, 0, 1) == 3)
  assert(stack:shakeOffsetForShakeFrames(6, 0, 1) == 1)
  assert(stack:shakeOffsetForShakeFrames(5, 0, 1) == 0)
  assert(stack:shakeOffsetForShakeFrames(4, 0, 1) == 1)
  assert(stack:shakeOffsetForShakeFrames(3, 0, 1) == 2)
  assert(stack:shakeOffsetForShakeFrames(2, 0, 1) == 1)
  assert(stack:shakeOffsetForShakeFrames(1, 0, 1) == 1)
  assert(stack:shakeOffsetForShakeFrames(0, 0, 1) == 0)
end

test(testShakeOffsetLargeGarbage)

-- Tests that having reduction properly reduces and rounds
local function testShakeOffsetReduction()
  local match = createEndlessClientMatch(2, defaultTheme)
  local stack = match.stacks[2]
  ---@cast stack PlayerStack
  assert(stack:shakeOffsetForShakeFrames(76, 0, 0.5) == 1)
  assert(stack:shakeOffsetForShakeFrames(75, 0, 0.5) == 0)
  assert(stack:shakeOffsetForShakeFrames(74, 0, 0.5) == 2)
  assert(stack:shakeOffsetForShakeFrames(73, 0, 0.5) == 4)
  assert(stack:shakeOffsetForShakeFrames(72, 0, 0.5) == 8)
  assert(stack:shakeOffsetForShakeFrames(71, 0, 0.5) == 12)
  assert(stack:shakeOffsetForShakeFrames(70, 0, 0.5) == 15)
  assert(stack:shakeOffsetForShakeFrames(69, 0, 0.5) == 17)
  assert(stack:shakeOffsetForShakeFrames(68, 0, 0.5) == 16)
  assert(stack:shakeOffsetForShakeFrames(67, 0, 0.5) == 14)
  assert(stack:shakeOffsetForShakeFrames(66, 0, 0.5) == 11)
  assert(stack:shakeOffsetForShakeFrames(65, 0, 0.5) == 7)
  assert(stack:shakeOffsetForShakeFrames(64, 0, 0.5) == 4)
  assert(stack:shakeOffsetForShakeFrames(63, 0, 0.5) == 1)
  assert(stack:shakeOffsetForShakeFrames(62, 0, 0.5) == 0)

  assert(stack:shakeOffsetForShakeFrames(12, 0, 0.5) == 0)
  assert(stack:shakeOffsetForShakeFrames(11, 0, 0.5) == 1)
  assert(stack:shakeOffsetForShakeFrames(10, 0, 0.5) == 2)
  assert(stack:shakeOffsetForShakeFrames(9, 0, 0.5) == 3)
  assert(stack:shakeOffsetForShakeFrames(8, 0, 0.5) == 2)
  assert(stack:shakeOffsetForShakeFrames(7, 0, 0.5) == 2)
  assert(stack:shakeOffsetForShakeFrames(6, 0, 0.5) == 1)
  assert(stack:shakeOffsetForShakeFrames(5, 0, 0.5) == 0)
  assert(stack:shakeOffsetForShakeFrames(4, 0, 0.5) == 1)
  assert(stack:shakeOffsetForShakeFrames(3, 0, 0.5) == 1)
  assert(stack:shakeOffsetForShakeFrames(2, 0, 0.5) == 1)
  assert(stack:shakeOffsetForShakeFrames(1, 0, 0.5) == 1)
  assert(stack:shakeOffsetForShakeFrames(0, 0, 0.5) == 0)
end

test(testShakeOffsetReduction)

local function testShakeOffsetMassiveReduction()
  local match = createEndlessClientMatch(2, defaultTheme)
  local stack = match.stacks[2]
  ---@cast stack PlayerStack
  assert(stack:shakeOffsetForShakeFrames(76, 0, 0.25) == 1)
  assert(stack:shakeOffsetForShakeFrames(75, 0, 0.25) == 0)
  assert(stack:shakeOffsetForShakeFrames(74, 0, 0.25) == 1)
  assert(stack:shakeOffsetForShakeFrames(73, 0, 0.25) == 2)
  assert(stack:shakeOffsetForShakeFrames(72, 0, 0.25) == 4)
  assert(stack:shakeOffsetForShakeFrames(71, 0, 0.25) == 6)
  assert(stack:shakeOffsetForShakeFrames(70, 0, 0.25) == 8)
  assert(stack:shakeOffsetForShakeFrames(69, 0, 0.25) == 9)
  assert(stack:shakeOffsetForShakeFrames(68, 0, 0.25) == 8)
  assert(stack:shakeOffsetForShakeFrames(67, 0, 0.25) == 7)
  assert(stack:shakeOffsetForShakeFrames(66, 0, 0.25) == 6)
  assert(stack:shakeOffsetForShakeFrames(65, 0, 0.25) == 4)
  assert(stack:shakeOffsetForShakeFrames(64, 0, 0.25) == 2)
  assert(stack:shakeOffsetForShakeFrames(63, 0, 0.25) == 1)
  assert(stack:shakeOffsetForShakeFrames(62, 0, 0.25) == 0)

  assert(stack:shakeOffsetForShakeFrames(12, 0, 0.25) == 0)
  assert(stack:shakeOffsetForShakeFrames(11, 0, 0.25) == 1)
  assert(stack:shakeOffsetForShakeFrames(10, 0, 0.25) == 1)
  assert(stack:shakeOffsetForShakeFrames(9, 0, 0.25) == 2)
  assert(stack:shakeOffsetForShakeFrames(8, 0, 0.25) == 1)
  assert(stack:shakeOffsetForShakeFrames(7, 0, 0.25) == 1)
  assert(stack:shakeOffsetForShakeFrames(6, 0, 0.25) == 1)
  assert(stack:shakeOffsetForShakeFrames(5, 0, 0.25) == 0)
  assert(stack:shakeOffsetForShakeFrames(4, 0, 0.25) == 1)
  assert(stack:shakeOffsetForShakeFrames(3, 0, 0.25) == 1)
  assert(stack:shakeOffsetForShakeFrames(2, 0, 0.25) == 1)
  assert(stack:shakeOffsetForShakeFrames(1, 0, 0.25) == 1)
  assert(stack:shakeOffsetForShakeFrames(0, 0, 0.25) == 0)
end

test(testShakeOffsetMassiveReduction)

local function testShakeInterpolate()
  local match = createEndlessClientMatch(2, defaultTheme)
  local stack = match.stacks[2]
  ---@cast stack PlayerStack
  assert(stack:shakeOffsetForShakeFrames(70, 0, 1) == 30)
  assert(stack:shakeOffsetForShakeFrames(16, 0, 1) == 8)
  assert(stack:shakeOffsetForShakeFrames(70, 16, 1) == 19)
  assert(stack:shakeOffsetForShakeFrames(2, 0, 1) == 1)
  assert(stack:shakeOffsetForShakeFrames(1, 0, 1) == 1)
  assert(stack:shakeOffsetForShakeFrames(2, 1, 1) == 1)
end

test(testShakeInterpolate)

-- Test for positioning system values (frameOriginX, panelOriginX, origin_x)
-- These values were captured BEFORE the moveToPosition refactor to ensure no regression
local function testCurrentStackPositioning()
  local match = createEndlessClientMatch(2, defaultTheme)
  
  local stack1 = match.stacks[1]
  local stack2 = match.stacks[2]
  
  -- Player 1 positioning values (renderIndex = 1) - ORIGINAL values before moveToPosition refactor
  assert(stack1.frameOriginX == 76) 
  assert(stack1.panelOriginX == 80) -- frameOriginX + panelOriginXOffset(4)
  assert(stack1.origin_x == 80)     -- Original positioning calculation
  
  -- Player 2 positioning values (renderIndex = 2) - ORIGINAL values before moveToPosition refactor
  -- Using math.floor to handle floating point precision
  assert(math.floor(stack2.frameOriginX) == 246) -- Original: 246.66666666667
  assert(math.floor(stack2.panelOriginX) == 250) -- Original: 250.66666666667  
  assert(math.floor(stack2.origin_x) == 346)     -- Original: 346.66666666667
end

test(testCurrentStackPositioning)

-- Test for center positioning (puzzle mode)
local function testCenterPositioning()
  local match = createEndlessClientMatch(1, defaultTheme)
  local stack = match.stacks[1]
  
  -- Position using center positioning method
  stack:moveToCenterPosition()
  
  -- Assert expected center positioning values
  assert(math.floor(stack.frameOriginX * 100) == 15933) -- 159.33333333333 * 100
  assert(math.floor(stack.panelOriginX * 100) == 16333) -- 163.33333333333 * 100  
  assert(math.floor(stack.origin_x * 100) == 16333)     -- 163.33333333333 * 100
  assert(stack.renderIndex == 1)
  assert(stack.mirror_x == 1)
end

test(testCenterPositioning)
