import XCTest
@testable import CollapseLogic

final class GameEngineTests: XCTestCase {

    // MARK: - Helpers

    /// Simple 5x5 grid, no walls, custom blocks
    private func makeState(
        width: Int = 5, height: Int = 5,
        blocks: [(x: Int, y: Int, color: BlockColor)],
        walls: [(x: Int, y: Int)] = [],
        par: Int = 5,
        allowsMerging: Bool = true
    ) -> GameState {
        let bl = blocks.enumerated().map { idx, b in
            Block(id: idx, position: GridPosition(x: b.x, y: b.y), color: b.color)
        }
        let ws = Set(walls.map { GridPosition(x: $0.x, y: $0.y) })
        return GameState(
            grid: GridSize(width: width, height: height),
            blocks: bl, walls: ws, moveCount: 0,
            par: par, objective: .clearAll,
            allowsMerging: allowsMerging, history: []
        )
    }

    // MARK: - Slide to boundary

    func testSlideRightToBoundary() {
        let state = makeState(blocks: [(x: 0, y: 2, color: .red)])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        XCTAssertTrue(result.isValid)
        XCTAssertEqual(result.newState.blocks[0].position, GridPosition(x: 4, y: 2))
    }

    func testSlideLeftToBoundary() {
        let state = makeState(blocks: [(x: 4, y: 2, color: .blue)])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .left)
        XCTAssertEqual(result.newState.blocks[0].position, GridPosition(x: 0, y: 2))
    }

    func testSlideUpToBoundary() {
        let state = makeState(blocks: [(x: 2, y: 4, color: .red)])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .up)
        XCTAssertEqual(result.newState.blocks[0].position, GridPosition(x: 2, y: 0))
    }

    func testSlideDownToBoundary() {
        let state = makeState(blocks: [(x: 2, y: 0, color: .blue)])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .down)
        XCTAssertEqual(result.newState.blocks[0].position, GridPosition(x: 2, y: 4))
    }

    // MARK: - Slide stops at wall

    func testStopsAtWall() {
        let state = makeState(
            blocks: [(x: 0, y: 2, color: .red)],
            walls: [(x: 3, y: 2)]
        )
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        XCTAssertEqual(result.newState.blocks[0].position, GridPosition(x: 2, y: 2))
    }

    func testStopsAtWallAbove() {
        let state = makeState(
            blocks: [(x: 2, y: 4, color: .blue)],
            walls: [(x: 2, y: 2)]
        )
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .up)
        XCTAssertEqual(result.newState.blocks[0].position, GridPosition(x: 2, y: 3))
    }

    // MARK: - Slide stops at another block

    func testStopsAtOtherBlock() {
        // Purple (secondary) at x=0, Red (primary) at x=3 → block stops, no merge
        let state = makeState(blocks: [
            (x: 0, y: 2, color: .purple),
            (x: 3, y: 2, color: .red)
        ])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        // Purple should land at x=2, stopped by red at x=3
        XCTAssertEqual(result.newState.blocks[0].position, GridPosition(x: 2, y: 2))
        XCTAssertEqual(result.newState.blocks.count, 2)
    }

    // MARK: - Same-color destroy

    func testSameColorDestroyBoth() {
        let state = makeState(blocks: [
            (x: 0, y: 2, color: .red),
            (x: 3, y: 2, color: .red)
        ])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        // Red slides right, lands at x=2, adjacent is red at x=3 → both destroyed
        XCTAssertEqual(result.newState.blocks.count, 0)
        // Confirm destroy event
        if case .destroyed = result.events.last { } else {
            XCTFail("Expected destroyed event")
        }
    }

    func testSameColorDestroyVertical() {
        let state = makeState(blocks: [
            (x: 2, y: 0, color: .blue),
            (x: 2, y: 4, color: .blue)
        ])
        // Push top blue down
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .down)
        XCTAssertEqual(result.newState.blocks.count, 0)
    }

    func testDifferentNonMergeableColorStops() {
        // Purple (secondary) at x=0, Green (secondary) at x=4 → different secondaries stop
        let state = makeState(blocks: [
            (x: 0, y: 2, color: .purple),
            (x: 4, y: 2, color: .green)
        ])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        // Purple lands at x=3, green stays at x=4, no interaction
        XCTAssertEqual(result.newState.blocks.count, 2)
    }

    // MARK: - Invalid moves

    func testInvalidMoveAtBoundary() {
        // Block already at left wall
        let state = makeState(blocks: [(x: 0, y: 2, color: .red)])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .left)
        XCTAssertFalse(result.isValid)
        XCTAssertEqual(result.newState.moveCount, 0)
    }

    func testInvalidMoveBlockedByWallImmediately() {
        let state = makeState(
            blocks: [(x: 2, y: 2, color: .red)],
            walls: [(x: 3, y: 2)]
        )
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        XCTAssertFalse(result.isValid)
        XCTAssertEqual(result.newState.moveCount, 0)
    }

    func testInvalidMoveBlockedByAdjacentBlock() {
        let state = makeState(blocks: [
            (x: 2, y: 2, color: .red),
            (x: 3, y: 2, color: .blue)
        ])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        XCTAssertFalse(result.isValid)
        XCTAssertEqual(result.newState.moveCount, 0)
    }

    func testOutOfRangeBlockIndex() {
        let state = makeState(blocks: [(x: 2, y: 2, color: .red)])
        let result = GameEngine.processMove(state: state, blockIndex: 5, direction: .right)
        XCTAssertFalse(result.isValid)
    }

    // MARK: - Move count

    func testMoveCountIncrementsOnValidMove() {
        let state = makeState(blocks: [(x: 0, y: 2, color: .red)])
        let r1 = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        XCTAssertEqual(r1.newState.moveCount, 1)
    }

    func testMoveCountDoesNotIncrementOnInvalidMove() {
        let state = makeState(blocks: [(x: 0, y: 2, color: .red)])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .left)
        XCTAssertEqual(result.newState.moveCount, 0)
    }

    func testMoveCountAccumulatesAcrossMoves() {
        var state = makeState(blocks: [
            (x: 0, y: 0, color: .red),
            (x: 0, y: 4, color: .blue)
        ])
        state = GameEngine.processMove(state: state, blockIndex: 0, direction: .right).newState
        state = GameEngine.processMove(state: state, blockIndex: 1, direction: .right).newState
        XCTAssertEqual(state.moveCount, 2)
    }

    // MARK: - Undo

    func testUndoRestoresPreviousState() {
        let initial = makeState(blocks: [(x: 0, y: 2, color: .red)])
        let afterMove = GameEngine.processMove(state: initial, blockIndex: 0, direction: .right).newState
        XCTAssertEqual(afterMove.blocks[0].position, GridPosition(x: 4, y: 2))

        let undone = GameEngine.undo(state: afterMove)
        XCTAssertEqual(undone.blocks[0].position, GridPosition(x: 0, y: 2))
        XCTAssertEqual(undone.moveCount, 0)
    }

    func testUndoMultipleSteps() {
        let s0 = makeState(blocks: [
            (x: 0, y: 0, color: .red),
            (x: 4, y: 4, color: .blue)
        ])
        let s1 = GameEngine.processMove(state: s0, blockIndex: 0, direction: .right).newState
        let s2 = GameEngine.processMove(state: s1, blockIndex: 1, direction: .left).newState

        let back1 = GameEngine.undo(state: s2)
        XCTAssertEqual(back1.moveCount, 1)
        let back0 = GameEngine.undo(state: back1)
        XCTAssertEqual(back0.moveCount, 0)
        XCTAssertEqual(back0.blocks[0].position, GridPosition(x: 0, y: 0))
    }

    func testUndoAtInitialStateReturnsUnchanged() {
        let state = makeState(blocks: [(x: 2, y: 2, color: .red)])
        let result = GameEngine.undo(state: state)
        XCTAssertEqual(result.moveCount, 0)
        XCTAssertEqual(result.blocks[0].position, GridPosition(x: 2, y: 2))
    }

    // MARK: - Win detection

    func testWinDetectedWhenAllBlocksGone() {
        let state = makeState(blocks: [
            (x: 0, y: 2, color: .red),
            (x: 3, y: 2, color: .red)
        ])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        XCTAssertTrue(result.newState.isWon)
    }

    func testNotWonWhenBlocksRemain() {
        let state = makeState(blocks: [
            (x: 0, y: 0, color: .red),
            (x: 4, y: 0, color: .red),
            (x: 2, y: 2, color: .blue)
        ])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        XCTAssertFalse(result.newState.isWon)
    }

    // MARK: - Stars

    func testThreeStarsAtPar() {
        let state = makeState(blocks: [(x: 0, y: 2, color: .red), (x: 3, y: 2, color: .red)], par: 1)
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        XCTAssertEqual(result.newState.currentStars, 3)
    }

    func testTwoStarsSlightlyOverPar() {
        let state = makeState(blocks: [(x: 0, y: 0, color: .red)], par: 1)
        var s = GameEngine.processMove(state: state, blockIndex: 0, direction: .right).newState
        // Force moveCount to par+1 by reflecting in a new state (simulate 2 moves)
        // We just check the stars formula directly
        XCTAssertEqual(state.stars(for: 2), 2)
        XCTAssertEqual(state.stars(for: 3), 2)
    }

    func testOneStarWellOverPar() {
        let state = makeState(blocks: [(x: 0, y: 0, color: .red), (x: 1, y: 0, color: .red)], par: 3)
        XCTAssertEqual(state.stars(for: 10), 1)
    }

    // MARK: - World 1 merge gating

    func testWorld1RedBluePrimariesStopWithoutMerging() {
        // Same input as testRedBlueProducesPurple, but with allowsMerging = false.
        // Expected: red lands adjacent to blue, both blocks remain, no purple created.
        let state = makeState(blocks: [
            (x: 0, y: 0, color: .red),
            (x: 4, y: 0, color: .blue)
        ], allowsMerging: false)
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        XCTAssertEqual(result.newState.blocks.count, 2)
        XCTAssertTrue(result.newState.blocks.contains(where: { $0.color == .red }))
        XCTAssertTrue(result.newState.blocks.contains(where: { $0.color == .blue }))
        XCTAssertFalse(result.newState.blocks.contains(where: { $0.color == .purple }))
        XCTAssertFalse(result.events.contains(where: {
            if case .merged = $0 { return true } else { return false }
        }))
    }

    func testWorld1LoadedLevelHasMergingDisabled() {
        // Sanity: a level with world: 1 should produce a state with allowsMerging == false.
        let json = """
        {
          "id": "w1_test", "world": 1, "level": 1, "title": "T",
          "grid": { "width": 5, "height": 5 },
          "blocks": [
            { "x": 0, "y": 0, "color": "red" },
            { "x": 4, "y": 0, "color": "blue" }
          ],
          "walls": [], "special_tiles": [],
          "objective": { "type": "clear_all" },
          "par": 1, "hints": []
        }
        """
        let data = Data(json.utf8)
        let def = try! JSONDecoder().decode(LevelDefinition.self, from: data)
        let state = LevelLoader.makeGameState(from: def)
        XCTAssertFalse(state.allowsMerging)
    }

    func testWorld2LoadedLevelHasMergingEnabled() {
        let json = """
        {
          "id": "w2_test", "world": 2, "level": 1, "title": "T",
          "grid": { "width": 5, "height": 5 },
          "blocks": [
            { "x": 0, "y": 0, "color": "red" },
            { "x": 4, "y": 0, "color": "blue" }
          ],
          "walls": [], "special_tiles": [],
          "objective": { "type": "clear_all" },
          "par": 1, "hints": []
        }
        """
        let data = Data(json.utf8)
        let def = try! JSONDecoder().decode(LevelDefinition.self, from: data)
        let state = LevelLoader.makeGameState(from: def)
        XCTAssertTrue(state.allowsMerging)
    }

    // MARK: - World 2 Merge Tests

    func testRedBlueProducesPurple() {
        // red at (0,0), blue at (4,0), push red right
        let state = makeState(blocks: [
            (x: 0, y: 0, color: .red),
            (x: 4, y: 0, color: .blue)
        ])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        // Both originals removed, one purple remains
        XCTAssertEqual(result.newState.blocks.count, 1)
        XCTAssertEqual(result.newState.blocks[0].color, .purple)
    }

    func testRedYellowProducesOrange() {
        let state = makeState(blocks: [
            (x: 0, y: 0, color: .red),
            (x: 4, y: 0, color: .yellow)
        ])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        XCTAssertEqual(result.newState.blocks.count, 1)
        XCTAssertEqual(result.newState.blocks[0].color, .orange)
    }

    func testBlueYellowProducesGreen() {
        let state = makeState(blocks: [
            (x: 0, y: 0, color: .blue),
            (x: 4, y: 0, color: .yellow)
        ])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        XCTAssertEqual(result.newState.blocks.count, 1)
        XCTAssertEqual(result.newState.blocks[0].color, .green)
    }

    func testMergedBlockAtStationaryPosition() {
        // red at (0,0), blue at (4,0), push red right
        // Merged block should be at blue's position (4,0), not red's landing (3,0)
        let state = makeState(blocks: [
            (x: 0, y: 0, color: .red),
            (x: 4, y: 0, color: .blue)
        ])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        XCTAssertEqual(result.newState.blocks[0].position, GridPosition(x: 4, y: 0))
    }

    func testSecondaryPlusPrimaryStops() {
        // purple moving into red → block stops, no merge, no destroy
        let state = makeState(blocks: [
            (x: 0, y: 0, color: .purple),
            (x: 4, y: 0, color: .red)
        ])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        XCTAssertEqual(result.newState.blocks.count, 2)
        // Purple should stop at x=3
        XCTAssertEqual(result.newState.blocks[0].position, GridPosition(x: 3, y: 0))
        // No merged or destroyed events (only slid)
        XCTAssertEqual(result.events.count, 1)
        if case .slid = result.events[0] { } else {
            XCTFail("Expected only a slid event")
        }
    }

    func testDifferentSecondariesStop() {
        // purple moving into green → block stops
        let state = makeState(blocks: [
            (x: 0, y: 0, color: .purple),
            (x: 4, y: 0, color: .green)
        ])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        XCTAssertEqual(result.newState.blocks.count, 2)
        XCTAssertEqual(result.newState.blocks[0].position, GridPosition(x: 3, y: 0))
    }

    func testSameSecondaryDestroysBoth() {
        // purple moving into purple → both destroyed
        let state = makeState(blocks: [
            (x: 0, y: 0, color: .purple),
            (x: 4, y: 0, color: .purple)
        ])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        XCTAssertEqual(result.newState.blocks.count, 0)
        XCTAssertTrue(result.events.contains(where: {
            if case .destroyed = $0 { return true }
            return false
        }))
    }

    func testMoveCountIncrementsOnMerge() {
        let state = makeState(blocks: [
            (x: 0, y: 0, color: .red),
            (x: 4, y: 0, color: .blue)
        ])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        XCTAssertEqual(result.newState.moveCount, 1)
    }

    func testMergeEmitsMergedEvent() {
        let state = makeState(blocks: [
            (x: 0, y: 0, color: .red),
            (x: 4, y: 0, color: .blue)
        ])
        let result = GameEngine.processMove(state: state, blockIndex: 0, direction: .right)
        let mergedEvent = result.events.first(where: {
            if case .merged = $0 { return true }
            return false
        })
        XCTAssertNotNil(mergedEvent)
        if case .merged(_, _, let resultColor, let at) = mergedEvent {
            XCTAssertEqual(resultColor, .purple)
            XCTAssertEqual(at, GridPosition(x: 4, y: 0))
        }
    }

    func testUndoAfterMergeRestoresState() {
        let initial = makeState(blocks: [
            (x: 0, y: 0, color: .red),
            (x: 4, y: 0, color: .blue)
        ])
        let afterMerge = GameEngine.processMove(state: initial, blockIndex: 0, direction: .right).newState
        // After merge: 1 purple block
        XCTAssertEqual(afterMerge.blocks.count, 1)
        XCTAssertEqual(afterMerge.blocks[0].color, .purple)

        // Undo should restore both original blocks
        let undone = GameEngine.undo(state: afterMerge)
        XCTAssertEqual(undone.blocks.count, 2)
        XCTAssertEqual(undone.moveCount, 0)
        // Original colors restored
        let colors = Set(undone.blocks.map(\.color))
        XCTAssertTrue(colors.contains(.red))
        XCTAssertTrue(colors.contains(.blue))
    }

    func testWinConditionAfterMerge() {
        // Board: one red + one blue. Merge them → purple remains → NOT a win.
        let state = makeState(blocks: [
            (x: 0, y: 0, color: .red),
            (x: 4, y: 0, color: .blue)
        ])
        let afterMerge = GameEngine.processMove(state: state, blockIndex: 0, direction: .right).newState
        XCTAssertFalse(afterMerge.isWon, "Merged block still remains — should not be won")

        // Now add a pre-placed purple to destroy the merged one.
        // Set up: purple at (0,2), another purple at (4,2). Push left purple right → destroy both → win.
        let state2 = makeState(blocks: [
            (x: 0, y: 2, color: .purple),
            (x: 4, y: 2, color: .purple)
        ])
        let afterDestroy = GameEngine.processMove(state: state2, blockIndex: 0, direction: .right).newState
        XCTAssertTrue(afterDestroy.isWon, "Both purples destroyed — should be won")
    }
}
