//
//  ItinerarySortOrderTests.swift
//  SportsTimeTests
//
//  Tests for sort order calculation (midpoint insertion algorithm).
//

import XCTest
@testable import SportsTime

private typealias H = ItineraryTestHelpers

final class ItinerarySortOrderTests: XCTestCase {

    private let testDate = H.testDate

    // MARK: - Midpoint Insertion Tests

    func test_sortOrder_dropBetweenItems_usesMidpoint() {
        // Given: Two items with sortOrder 1.0 and 3.0
        // When: Dropping between them
        // Then: New item should get sortOrder 2.0 (midpoint)

        let item1 = H.makeCustomItem(day: 1, sortOrder: 1.0, title: "First")
        let item2 = H.makeCustomItem(day: 1, sortOrder: 3.0, title: "Third")
        let movingItem = H.makeCustomItem(day: 1, sortOrder: 5.0, title: "Moving")

        let dayData = ItineraryDayData(
            id: 1,
            dayNumber: 1,
            date: testDate,
            games: [],
            items: [.customItem(item1), .customItem(item2), .customItem(movingItem)],
            travelBefore: nil
        )

        let controller = ItineraryTableViewController(style: .plain)
        var capturedSortOrder: Double = 0
        controller.onCustomItemMoved = { _, _, sortOrder in
            capturedSortOrder = sortOrder
        }
        controller.reloadData(days: [dayData], travelValidRanges: [:], itineraryItems: [item1, item2, movingItem])

        // Simulate move: row 3 (movingItem) to row 2 (between item1 and item2)
        // Rows: 0=header, 1=item1, 2=item2, 3=movingItem
        controller.tableView(controller.tableView, moveRowAt: IndexPath(row: 3, section: 0), to: IndexPath(row: 2, section: 0))

        // The new sortOrder should be midpoint between 1.0 and 3.0 = 2.0
        XCTAssertEqual(capturedSortOrder, 2.0, accuracy: 0.01, "Sort order should be midpoint (2.0)")
    }

    func test_sortOrder_dropAtEnd_incrementsLastSortOrder() {
        // Given: An item with sortOrder 2.0
        // When: Dropping after it
        // Then: New item should get sortOrder 3.0 (last + 1.0)

        let existingItem = H.makeCustomItem(day: 1, sortOrder: 2.0, title: "Existing")
        let movingItem = H.makeCustomItem(day: 2, sortOrder: 1.0, title: "Moving")

        let day1 = ItineraryDayData(id: 1, dayNumber: 1, date: testDate, games: [], items: [.customItem(existingItem)], travelBefore: nil)
        let day2 = ItineraryDayData(id: 2, dayNumber: 2, date: H.dayAfter(testDate), games: [], items: [.customItem(movingItem)], travelBefore: nil)

        let controller = ItineraryTableViewController(style: .plain)
        var capturedSortOrder: Double = 0
        controller.onCustomItemMoved = { _, _, sortOrder in
            capturedSortOrder = sortOrder
        }
        controller.reloadData(days: [day1, day2], travelValidRanges: [:], itineraryItems: [existingItem, movingItem])

        // Move item from Day 2 to end of Day 1
        // Rows: 0=Day1 header, 1=existingItem, 2=Day2 header, 3=movingItem
        // Move row 3 to row 2 (after existingItem, before Day2 header)
        controller.tableView(controller.tableView, moveRowAt: IndexPath(row: 3, section: 0), to: IndexPath(row: 2, section: 0))

        // New sortOrder should be 2.0 + 1.0 = 3.0
        XCTAssertEqual(capturedSortOrder, 3.0, accuracy: 0.01, "Sort order should be last + 1.0 = 3.0")
    }

    func test_sortOrder_dropAsFirstItem_halvesPreviousSortOrder() {
        // Given: An item with sortOrder 2.0
        // When: Dropping before it as first item
        // Then: New item should get sortOrder 1.0 (first / 2.0)

        let existingItem = H.makeCustomItem(day: 1, sortOrder: 2.0, title: "Existing")
        let movingItem = H.makeCustomItem(day: 2, sortOrder: 1.0, title: "Moving")

        let day1 = ItineraryDayData(id: 1, dayNumber: 1, date: testDate, games: [], items: [.customItem(existingItem)], travelBefore: nil)
        let day2 = ItineraryDayData(id: 2, dayNumber: 2, date: H.dayAfter(testDate), games: [], items: [.customItem(movingItem)], travelBefore: nil)

        let controller = ItineraryTableViewController(style: .plain)
        var capturedSortOrder: Double = 0
        controller.onCustomItemMoved = { _, _, sortOrder in
            capturedSortOrder = sortOrder
        }
        controller.reloadData(days: [day1, day2], travelValidRanges: [:], itineraryItems: [existingItem, movingItem])

        // Move item from Day 2 to before existingItem
        // Rows: 0=Day1 header, 1=existingItem, 2=Day2 header, 3=movingItem
        // Move row 3 to row 1 (before existingItem, after header)
        controller.tableView(controller.tableView, moveRowAt: IndexPath(row: 3, section: 0), to: IndexPath(row: 1, section: 0))

        // New sortOrder should be 2.0 / 2.0 = 1.0
        XCTAssertEqual(capturedSortOrder, 1.0, accuracy: 0.01, "Sort order should be first / 2.0 = 1.0")
    }

    func test_sortOrder_emptyDay_defaultsTo1() {
        // Given: An empty day
        // When: Dropping first item
        // Then: Sort order should be 1.0

        let movingItem = H.makeCustomItem(day: 2, sortOrder: 5.0, title: "Moving")

        let day1 = ItineraryDayData(id: 1, dayNumber: 1, date: testDate, games: [], items: [], travelBefore: nil)
        let day2 = ItineraryDayData(id: 2, dayNumber: 2, date: H.dayAfter(testDate), games: [], items: [.customItem(movingItem)], travelBefore: nil)

        let controller = ItineraryTableViewController(style: .plain)
        var capturedSortOrder: Double = 0
        controller.onCustomItemMoved = { _, _, sortOrder in
            capturedSortOrder = sortOrder
        }
        controller.reloadData(days: [day1, day2], travelValidRanges: [:], itineraryItems: [movingItem])

        // Move item to empty Day 1
        // Rows: 0=Day1 header, 1=Day2 header, 2=movingItem
        // Move row 2 to row 1 (after Day1 header)
        controller.tableView(controller.tableView, moveRowAt: IndexPath(row: 2, section: 0), to: IndexPath(row: 1, section: 0))

        XCTAssertEqual(capturedSortOrder, 1.0, accuracy: 0.01, "Sort order on empty day should be 1.0")
    }

    // MARK: - scanForward Bug Tests

    /// This test explicitly targets the scanForward(from: row) bug.
    /// After inserting the moved item at `row`, scanForward finds THE MOVED ITEM ITSELF
    /// and returns its old sortOrder instead of the item that should come after.
    func test_sortOrder_scanForwardBug_shouldNotFindMovedItemItself() {
        // Given: Items with sortOrders 10.0, 20.0, 30.0
        // When: Moving item at 30.0 to between 10.0 and 20.0
        // Expected: New sortOrder = (10.0 + 20.0) / 2 = 15.0
        // Actual Bug: scanForward finds moved item (30.0), returns (10.0 + 30.0) / 2 = 20.0

        let item1 = H.makeCustomItem(day: 1, sortOrder: 10.0, title: "A")
        let item2 = H.makeCustomItem(day: 1, sortOrder: 20.0, title: "B")
        let movingItem = H.makeCustomItem(day: 1, sortOrder: 30.0, title: "Moving")

        let dayData = ItineraryDayData(
            id: 1,
            dayNumber: 1,
            date: testDate,
            games: [],
            items: [.customItem(item1), .customItem(item2), .customItem(movingItem)],
            travelBefore: nil
        )

        let controller = ItineraryTableViewController(style: .plain)
        var capturedSortOrder: Double = 0
        controller.onCustomItemMoved = { _, _, sortOrder in
            capturedSortOrder = sortOrder
        }
        controller.reloadData(days: [dayData], travelValidRanges: [:], itineraryItems: [item1, item2, movingItem])

        // Rows: 0=header, 1=item1(10), 2=item2(20), 3=movingItem(30)
        // Move row 3 to row 2 (between item1 and item2)
        controller.tableView(controller.tableView, moveRowAt: IndexPath(row: 3, section: 0), to: IndexPath(row: 2, section: 0))

        // Expected: midpoint of 10.0 and 20.0 = 15.0
        // Bug produces: midpoint of 10.0 and 30.0 = 20.0
        XCTAssertEqual(capturedSortOrder, 15.0, accuracy: 0.01,
            "Sort order should be midpoint of surrounding items (15.0), not including moved item's old sortOrder")
    }

    // MARK: - Precision Tests

    func test_sortOrder_afterManyMidpointInsertions_maintainsPrecision() {
        // Verify that many midpoint insertions don't cause precision issues
        var sortOrders: [Double] = [1.0, 2.0]

        // Insert between 1.0 and 2.0 repeatedly (simulating many reorders)
        for _ in 0..<50 {
            let midpoint = (sortOrders[0] + sortOrders[1]) / 2.0
            sortOrders.insert(midpoint, at: 1)
        }

        // All values should still be distinct and properly ordered
        for i in 0..<(sortOrders.count - 1) {
            XCTAssertLessThan(sortOrders[i], sortOrders[i + 1], "Sort orders should remain properly ordered after many insertions")
            XCTAssertNotEqual(sortOrders[i], sortOrders[i + 1], "Sort orders should remain distinct after many insertions")
        }
    }

    // MARK: - Before/After Games Tests

    func test_moveItem_beforeGames_getsNegativeSortOrder() {
        // Given: A game at sortOrder 0 (implicit), item after game at sortOrder 1.0
        // When: Moving item to before games
        // Then: Should get negative sortOrder (e.g., -1.0)

        let games = [H.makeRichGame(city: "Detroit", hour: 19)]
        let item = H.makeCustomItem(day: 1, sortOrder: 1.0, title: "AfterGame")

        let dayData = ItineraryDayData(
            id: 1,
            dayNumber: 1,
            date: testDate,
            games: games,
            items: [.customItem(item)],
            travelBefore: nil
        )

        var capturedSortOrder: Double = 0
        let controller = ItineraryTableViewController(style: .plain)
        controller.onCustomItemMoved = { _, _, sortOrder in
            capturedSortOrder = sortOrder
        }
        controller.reloadData(days: [dayData], travelValidRanges: [:], itineraryItems: [item])

        // Rows: 0=header, 1=games, 2=item
        // Move item (row 2) to row 1 (before games, after header)
        controller.tableView(controller.tableView, moveRowAt: IndexPath(row: 2, section: 0), to: IndexPath(row: 1, section: 0))

        XCTAssertLessThan(capturedSortOrder, 0, "Item moved before games should have negative sortOrder")
    }

    func test_moveItem_afterGames_getsPositiveSortOrder() {
        // Given: A game, item before game at sortOrder -1.0
        // When: Moving item to after games
        // Then: Should get positive sortOrder

        let games = [H.makeRichGame(city: "Detroit", hour: 19)]
        let item = H.makeCustomItem(day: 1, sortOrder: -1.0, title: "BeforeGame")

        let dayData = ItineraryDayData(
            id: 1,
            dayNumber: 1,
            date: testDate,
            games: games,
            items: [.customItem(item)],
            travelBefore: nil
        )

        var capturedSortOrder: Double = 0
        let controller = ItineraryTableViewController(style: .plain)
        controller.onCustomItemMoved = { _, _, sortOrder in
            capturedSortOrder = sortOrder
        }
        controller.reloadData(days: [dayData], travelValidRanges: [:], itineraryItems: [item])

        // Rows: 0=header, 1=item(-1.0), 2=games
        // Move item (row 1) to row 2 (after games)
        controller.tableView(controller.tableView, moveRowAt: IndexPath(row: 1, section: 0), to: IndexPath(row: 2, section: 0))

        XCTAssertGreaterThan(capturedSortOrder, 0, "Item moved after games should have positive sortOrder")
    }
}
