//
//  GameDAGRouterScaleTests.swift
//  SportsTimeTests
//
//  Phase 3: GameDAGRouter Scale & Performance Tests
//  Stress tests for large datasets. May run for extended periods.
//

import Testing
import Foundation
@testable import SportsTime

@Suite("GameDAGRouter Scale & Performance Tests")
struct GameDAGRouterScaleTests {

    // MARK: - 3A: Scale Tests

    @Test("3.1 - 5 games completes within 5 minutes")
    func test_findRoutes_5Games_CompletesWithin5Minutes() async throws {
        let config = FixtureGenerator.Configuration(
            seed: 31,
            gameCount: 5,
            stadiumCount: 5,
            teamCount: 5,
            geographicSpread: .regional
        )
        let data = FixtureGenerator.generate(with: config)

        let startTime = Date()
        let routes = GameDAGRouter.findRoutes(
            games: data.games,
            stadiums: data.stadiumsById,
            constraints: .default
        )
        let elapsed = Date().timeIntervalSince(startTime)

        #expect(elapsed < TestConstants.performanceTimeout, "Should complete within 5 minutes")
        #expect(!routes.isEmpty, "Should produce at least one route")

        // Verify route validity
        for route in routes {
            #expect(!route.isEmpty, "Routes should not be empty")
            for i in 0..<(route.count - 1) {
                #expect(route[i].startTime <= route[i + 1].startTime, "Games should be chronologically ordered")
            }
        }

        print("3.1 - 5 games: \(routes.count) routes in \(String(format: "%.2f", elapsed))s")
    }

    @Test("3.2 - 50 games completes within 5 minutes")
    func test_findRoutes_50Games_CompletesWithin5Minutes() async throws {
        let config = FixtureGenerator.Configuration(
            seed: 32,
            gameCount: 50,
            stadiumCount: 15,
            teamCount: 15,
            geographicSpread: .regional
        )
        let data = FixtureGenerator.generate(with: config)

        let startTime = Date()
        let routes = GameDAGRouter.findRoutes(
            games: data.games,
            stadiums: data.stadiumsById,
            constraints: .default
        )
        let elapsed = Date().timeIntervalSince(startTime)

        #expect(elapsed < TestConstants.performanceTimeout, "Should complete within 5 minutes")
        #expect(!routes.isEmpty, "Should produce routes")

        // Verify route validity
        for route in routes {
            for i in 0..<(route.count - 1) {
                #expect(route[i].startTime <= route[i + 1].startTime, "Games should be chronologically ordered")
            }
        }

        print("3.2 - 50 games: \(routes.count) routes in \(String(format: "%.2f", elapsed))s")
    }

    @Test("3.3 - 500 games completes within 5 minutes")
    func test_findRoutes_500Games_CompletesWithin5Minutes() async throws {
        let config = FixtureGenerator.Configuration.medium // 500 games
        let data = FixtureGenerator.generate(with: config)

        let startTime = Date()
        let routes = GameDAGRouter.findRoutes(
            games: data.games,
            stadiums: data.stadiumsById,
            constraints: .default
        )
        let elapsed = Date().timeIntervalSince(startTime)

        #expect(elapsed < TestConstants.performanceTimeout, "Should complete within 5 minutes")
        #expect(!routes.isEmpty, "Should produce routes")

        // Verify route validity
        for route in routes {
            for i in 0..<(route.count - 1) {
                #expect(route[i].startTime <= route[i + 1].startTime, "Games should be chronologically ordered")
            }
        }

        print("3.3 - 500 games: \(routes.count) routes in \(String(format: "%.2f", elapsed))s")

        // Record baseline if not set
        if TestConstants.baseline500Games == 0 {
            print("BASELINE 500 games: \(elapsed)s (record this in TestConstants)")
        }
    }

    @Test("3.4 - 2000 games completes within 5 minutes")
    func test_findRoutes_2000Games_CompletesWithin5Minutes() async throws {
        let config = FixtureGenerator.Configuration.large // 2000 games
        let data = FixtureGenerator.generate(with: config)

        let startTime = Date()
        let routes = GameDAGRouter.findRoutes(
            games: data.games,
            stadiums: data.stadiumsById,
            constraints: .default
        )
        let elapsed = Date().timeIntervalSince(startTime)

        #expect(elapsed < TestConstants.performanceTimeout, "Should complete within 5 minutes")
        #expect(!routes.isEmpty, "Should produce routes")

        // Verify route validity
        for route in routes {
            for i in 0..<(route.count - 1) {
                #expect(route[i].startTime <= route[i + 1].startTime, "Games should be chronologically ordered")
            }
        }

        print("3.4 - 2000 games: \(routes.count) routes in \(String(format: "%.2f", elapsed))s")

        // Record baseline if not set
        if TestConstants.baseline2000Games == 0 {
            print("BASELINE 2000 games: \(elapsed)s (record this in TestConstants)")
        }
    }

    @Test("3.5 - 10000 games completes within 5 minutes")
    func test_findRoutes_10000Games_CompletesWithin5Minutes() async throws {
        let config = FixtureGenerator.Configuration.stress // 10000 games
        let data = FixtureGenerator.generate(with: config)

        let startTime = Date()
        let routes = GameDAGRouter.findRoutes(
            games: data.games,
            stadiums: data.stadiumsById,
            constraints: .default
        )
        let elapsed = Date().timeIntervalSince(startTime)

        #expect(elapsed < TestConstants.performanceTimeout, "Should complete within 5 minutes")
        #expect(!routes.isEmpty, "Should produce routes")

        // Verify route validity
        for route in routes {
            for i in 0..<(route.count - 1) {
                #expect(route[i].startTime <= route[i + 1].startTime, "Games should be chronologically ordered")
            }
        }

        print("3.5 - 10000 games: \(routes.count) routes in \(String(format: "%.2f", elapsed))s")

        // Record baseline if not set
        if TestConstants.baseline10000Games == 0 {
            print("BASELINE 10000 games: \(elapsed)s (record this in TestConstants)")
        }
    }

    @Test("3.6 - 50000 nodes completes within 5 minutes")
    func test_findRoutes_50000Nodes_CompletesWithin5Minutes() async throws {
        // Extreme stress test - 50000 games
        let config = FixtureGenerator.Configuration(
            seed: 36,
            gameCount: 50000,
            stadiumCount: 30,
            teamCount: 60,
            geographicSpread: .nationwide
        )
        let data = FixtureGenerator.generate(with: config)

        let startTime = Date()
        let routes = GameDAGRouter.findRoutes(
            games: data.games,
            stadiums: data.stadiumsById,
            constraints: .default,
            beamWidth: 25 // Reduced beam width for extreme scale
        )
        let elapsed = Date().timeIntervalSince(startTime)

        #expect(elapsed < TestConstants.performanceTimeout, "Should complete within 5 minutes (may need timeout adjustment)")
        // Routes may be empty for extreme stress test - that's acceptable if it completes

        print("3.6 - 50000 games: \(routes.count) routes in \(String(format: "%.2f", elapsed))s")
    }

    // MARK: - 3B: Performance Baselines

    @Test("3.7 - Record baseline times for 500/2000/10000 games")
    func test_recordBaselineTimes() async throws {
        // Run each size and record times for baseline establishment
        var baselines: [(size: Int, time: TimeInterval)] = []

        // 500 games
        let config500 = FixtureGenerator.Configuration.medium
        let data500 = FixtureGenerator.generate(with: config500)
        let start500 = Date()
        _ = GameDAGRouter.findRoutes(
            games: data500.games,
            stadiums: data500.stadiumsById,
            constraints: .default
        )
        let elapsed500 = Date().timeIntervalSince(start500)
        baselines.append((500, elapsed500))

        // 2000 games
        let config2000 = FixtureGenerator.Configuration.large
        let data2000 = FixtureGenerator.generate(with: config2000)
        let start2000 = Date()
        _ = GameDAGRouter.findRoutes(
            games: data2000.games,
            stadiums: data2000.stadiumsById,
            constraints: .default
        )
        let elapsed2000 = Date().timeIntervalSince(start2000)
        baselines.append((2000, elapsed2000))

        // 10000 games
        let config10000 = FixtureGenerator.Configuration.stress
        let data10000 = FixtureGenerator.generate(with: config10000)
        let start10000 = Date()
        _ = GameDAGRouter.findRoutes(
            games: data10000.games,
            stadiums: data10000.stadiumsById,
            constraints: .default
        )
        let elapsed10000 = Date().timeIntervalSince(start10000)
        baselines.append((10000, elapsed10000))

        // Print baselines for recording
        print("\n=== PERFORMANCE BASELINES ===")
        for baseline in baselines {
            print("\(baseline.size) games: \(String(format: "%.3f", baseline.time))s")
        }
        print("==============================\n")

        // All should complete within timeout
        for baseline in baselines {
            #expect(baseline.time < TestConstants.performanceTimeout, "\(baseline.size) games should complete within timeout")
        }
    }

    @Test("3.8 - Performance regression assertions")
    func test_performanceRegressionAssertions() async throws {
        // Skip if baselines not yet established
        guard TestConstants.baseline500Games > 0 else {
            print("Skipping regression test - baselines not yet recorded")
            return
        }

        // 500 games - compare to baseline with 50% tolerance
        let config500 = FixtureGenerator.Configuration.medium
        let data500 = FixtureGenerator.generate(with: config500)
        let start500 = Date()
        _ = GameDAGRouter.findRoutes(
            games: data500.games,
            stadiums: data500.stadiumsById,
            constraints: .default
        )
        let elapsed500 = Date().timeIntervalSince(start500)

        let tolerance500 = TestConstants.baseline500Games * 1.5
        #expect(elapsed500 <= tolerance500, "500 games should not regress more than 50% from baseline (\(TestConstants.baseline500Games)s)")

        // 2000 games
        if TestConstants.baseline2000Games > 0 {
            let config2000 = FixtureGenerator.Configuration.large
            let data2000 = FixtureGenerator.generate(with: config2000)
            let start2000 = Date()
            _ = GameDAGRouter.findRoutes(
                games: data2000.games,
                stadiums: data2000.stadiumsById,
                constraints: .default
            )
            let elapsed2000 = Date().timeIntervalSince(start2000)

            let tolerance2000 = TestConstants.baseline2000Games * 1.5
            #expect(elapsed2000 <= tolerance2000, "2000 games should not regress more than 50% from baseline")
        }

        // 10000 games
        if TestConstants.baseline10000Games > 0 {
            let config10000 = FixtureGenerator.Configuration.stress
            let data10000 = FixtureGenerator.generate(with: config10000)
            let start10000 = Date()
            _ = GameDAGRouter.findRoutes(
                games: data10000.games,
                stadiums: data10000.stadiumsById,
                constraints: .default
            )
            let elapsed10000 = Date().timeIntervalSince(start10000)

            let tolerance10000 = TestConstants.baseline10000Games * 1.5
            #expect(elapsed10000 <= tolerance10000, "10000 games should not regress more than 50% from baseline")
        }
    }

    // MARK: - 3C: Memory Tests

    @Test("3.9 - Repeated calls show no memory leak")
    func test_findRoutes_RepeatedCalls_NoMemoryLeak() async throws {
        // Run 100 iterations with medium dataset and verify no memory growth
        let config = FixtureGenerator.Configuration(
            seed: 39,
            gameCount: 100,
            stadiumCount: 15,
            teamCount: 15,
            geographicSpread: .regional
        )
        let data = FixtureGenerator.generate(with: config)

        // Get initial memory footprint (rough approximation)
        let initialMemory = getMemoryUsageMB()

        // Run 100 iterations
        for iteration in 0..<100 {
            autoreleasepool {
                _ = GameDAGRouter.findRoutes(
                    games: data.games,
                    stadiums: data.stadiumsById,
                    constraints: .default
                )
            }

            // Check memory every 20 iterations
            if iteration > 0 && iteration % 20 == 0 {
                let currentMemory = getMemoryUsageMB()
                print("Iteration \(iteration): Memory usage \(String(format: "%.1f", currentMemory)) MB")
            }
        }

        let finalMemory = getMemoryUsageMB()
        let memoryGrowth = finalMemory - initialMemory

        print("Memory test: Initial=\(String(format: "%.1f", initialMemory))MB, Final=\(String(format: "%.1f", finalMemory))MB, Growth=\(String(format: "%.1f", memoryGrowth))MB")

        // Allow up to 50MB growth (reasonable for 100 iterations with route caching)
        #expect(memoryGrowth < 50.0, "Memory should not grow excessively over 100 iterations (grew \(memoryGrowth)MB)")
    }

    @Test("3.10 - Large dataset memory bounded")
    func test_findRoutes_LargeDataset_MemoryBounded() async throws {
        // 10K games should not exceed reasonable memory
        let config = FixtureGenerator.Configuration.stress // 10000 games
        let data = FixtureGenerator.generate(with: config)

        let beforeMemory = getMemoryUsageMB()

        _ = GameDAGRouter.findRoutes(
            games: data.games,
            stadiums: data.stadiumsById,
            constraints: .default
        )

        let afterMemory = getMemoryUsageMB()
        let memoryUsed = afterMemory - beforeMemory

        print("10K games memory: Before=\(String(format: "%.1f", beforeMemory))MB, After=\(String(format: "%.1f", afterMemory))MB, Used=\(String(format: "%.1f", memoryUsed))MB")

        // 10K games with 30 stadiums should not use more than 500MB
        #expect(memoryUsed < 500.0, "10K games should not use more than 500MB (used \(memoryUsed)MB)")
    }

    // MARK: - Helper Functions

    /// Returns current memory usage in MB (approximate)
    private func getMemoryUsageMB() -> Double {
        var info = mach_task_basic_info()
        var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
        let result = withUnsafeMutablePointer(to: &info) {
            $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
                task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
            }
        }
        guard result == KERN_SUCCESS else { return 0 }
        return Double(info.resident_size) / 1024.0 / 1024.0
    }
}
