import XCTest
import Combine
@testable import CommandNotch

@MainActor
final class NotchOrchestratorTests: XCTestCase {
    func testHoverOpenSchedulesOpenAfterDelay() {
        let screenID = "screen-a"
        let screen = makeScreenContext(screenID: screenID)
        let registry = TestScreenRegistry(activeScreenID: screenID, screens: [screen])
        let host = TestNotchPresentationHost()
        let scheduler = TestScheduler()
        let orchestrator = NotchOrchestrator(
            screenRegistry: registry,
            host: host,
            settingsController: makeSettingsController(),
            scheduler: scheduler
        )

        orchestrator.handleHoverChange(true, for: screenID)
        XCTAssertEqual(screen.notchState, .closed)

        scheduler.runScheduledActions()

        XCTAssertEqual(screen.notchState, .open)
        XCTAssertEqual(host.openedScreenIDs, [screenID])
    }

    func testHoverExitCancelsPendingOpen() {
        let screenID = "screen-a"
        let screen = makeScreenContext(screenID: screenID)
        let registry = TestScreenRegistry(activeScreenID: screenID, screens: [screen])
        let host = TestNotchPresentationHost()
        let scheduler = TestScheduler()
        let orchestrator = NotchOrchestrator(
            screenRegistry: registry,
            host: host,
            settingsController: makeSettingsController(),
            scheduler: scheduler
        )

        orchestrator.handleHoverChange(true, for: screenID)
        orchestrator.handleHoverChange(false, for: screenID)

        scheduler.runScheduledActions()

        XCTAssertEqual(screen.notchState, .closed)
        XCTAssertTrue(host.openedScreenIDs.isEmpty)
    }

    func testCloseWhileHoveringSuppressesReopenUntilHoverExit() {
        let screenID = "screen-a"
        let screen = makeScreenContext(screenID: screenID)
        let registry = TestScreenRegistry(activeScreenID: screenID, screens: [screen])
        let host = TestNotchPresentationHost()
        let scheduler = TestScheduler()
        let orchestrator = NotchOrchestrator(
            screenRegistry: registry,
            host: host,
            settingsController: makeSettingsController(),
            scheduler: scheduler
        )

        orchestrator.handleHoverChange(true, for: screenID)
        scheduler.runScheduledActions()
        XCTAssertEqual(screen.notchState, .open)

        orchestrator.handleHoverChange(true, for: screenID)
        orchestrator.close(screenID: screenID)
        scheduler.runScheduledActions()

        XCTAssertEqual(screen.notchState, .closed)
        XCTAssertFalse(screen.isCloseTransitionActive)
        XCTAssertTrue(screen.suppressHoverOpenUntilHoverExit)
        XCTAssertEqual(host.closedScreenIDs, [screenID])

        scheduler.runScheduledActions()
        XCTAssertEqual(screen.notchState, .closed)
        XCTAssertEqual(host.openedScreenIDs, [screenID])

        orchestrator.handleHoverChange(false, for: screenID)
        XCTAssertFalse(screen.suppressHoverOpenUntilHoverExit)
    }

    func testOpeningSharedWorkspaceOnAnotherScreenClosesPreviousPresenter() {
        let workspaceID = UUID()
        let firstScreen = makeScreenContext(screenID: "screen-a", workspaceID: workspaceID)
        let secondScreen = makeScreenContext(screenID: "screen-b", workspaceID: workspaceID)
        let registry = TestScreenRegistry(activeScreenID: "screen-b", screens: [firstScreen, secondScreen])
        let host = TestNotchPresentationHost()
        let orchestrator = NotchOrchestrator(
            screenRegistry: registry,
            host: host,
            settingsController: makeSettingsController(),
            scheduler: TestScheduler()
        )

        orchestrator.open(screenID: "screen-a")
        orchestrator.open(screenID: "screen-b")

        XCTAssertEqual(firstScreen.notchState, .closed)
        XCTAssertEqual(secondScreen.notchState, .open)
        XCTAssertEqual(host.closedScreenIDs, ["screen-a"])
        XCTAssertEqual(registry.presentingScreenID(for: workspaceID), "screen-b")
    }

    func testOpeningDifferentWorkspaceDoesNotCloseOtherOpenScreen() {
        let firstScreen = makeScreenContext(screenID: "screen-a", workspaceID: UUID())
        let secondScreen = makeScreenContext(screenID: "screen-b", workspaceID: UUID())
        let registry = TestScreenRegistry(activeScreenID: "screen-b", screens: [firstScreen, secondScreen])
        let host = TestNotchPresentationHost()
        let orchestrator = NotchOrchestrator(
            screenRegistry: registry,
            host: host,
            settingsController: makeSettingsController(),
            scheduler: TestScheduler()
        )

        orchestrator.open(screenID: "screen-a")
        orchestrator.open(screenID: "screen-b")

        XCTAssertEqual(firstScreen.notchState, .open)
        XCTAssertEqual(secondScreen.notchState, .open)
        XCTAssertTrue(host.closedScreenIDs.isEmpty)
        XCTAssertEqual(registry.presentingScreenID(for: firstScreen.workspaceID), "screen-a")
        XCTAssertEqual(registry.presentingScreenID(for: secondScreen.workspaceID), "screen-b")
    }

    private func makeScreenContext(screenID: ScreenID, workspaceID: WorkspaceID = UUID()) -> ScreenContext {
        ScreenContext(
            id: screenID,
            workspaceID: workspaceID,
            settingsController: makeSettingsController(),
            screenProvider: { _ in nil }
        )
    }

    private func makeSettingsController() -> AppSettingsController {
        let store = TestOrchestratorSettingsStore()
        var settings = AppSettings.default
        settings.behavior.openNotchOnHover = true
        settings.behavior.minimumHoverDuration = 0.3
        store.storedSettings = settings
        return AppSettingsController(store: store)
    }
}

@MainActor
private final class TestScreenRegistry: ScreenRegistryType {
    private let activeID: ScreenID
    private var screensByID: [ScreenID: ScreenContext]
    private var workspacePresenters: [WorkspaceID: ScreenID] = [:]

    init(activeScreenID: ScreenID, screens: [ScreenContext]) {
        self.activeID = activeScreenID
        self.screensByID = Dictionary(uniqueKeysWithValues: screens.map { ($0.id, $0) })
    }

    func allScreens() -> [ScreenContext] {
        Array(screensByID.values)
    }

    func screenContext(for id: ScreenID) -> ScreenContext? {
        screensByID[id]
    }

    func activeScreenID() -> ScreenID? {
        activeID
    }

    func presentingScreenID(for workspaceID: WorkspaceID) -> ScreenID? {
        workspacePresenters[workspaceID]
    }

    func claimWorkspacePresentation(for screenID: ScreenID) -> ScreenID? {
        guard let workspaceID = screensByID[screenID]?.workspaceID else { return nil }
        let previous = workspacePresenters[workspaceID]
        workspacePresenters[workspaceID] = screenID
        return previous == screenID ? nil : previous
    }

    func releaseWorkspacePresentation(for screenID: ScreenID) {
        workspacePresenters = workspacePresenters.filter { $0.value != screenID }
    }
}

@MainActor
private final class TestNotchPresentationHost: NotchPresentationHost {
    var openedScreenIDs: [ScreenID] = []
    var closedScreenIDs: [ScreenID] = []

    func canPresentNotch(for screenID: ScreenID) -> Bool {
        true
    }

    func performOpenPresentation(for screenID: ScreenID) {
        openedScreenIDs.append(screenID)
    }

    func performClosePresentation(for screenID: ScreenID) {
        closedScreenIDs.append(screenID)
    }
}

private final class TestScheduler: SchedulerType {
    private final class ScheduledAction {
        let action: @MainActor () -> Void
        var isCancelled = false

        init(action: @escaping @MainActor () -> Void) {
            self.action = action
        }
    }

    private var scheduledActions: [ScheduledAction] = []

    @MainActor
    func schedule(after interval: TimeInterval, action: @escaping @MainActor () -> Void) -> AnyCancellable {
        let scheduledAction = ScheduledAction(action: action)
        scheduledActions.append(scheduledAction)

        return AnyCancellable {
            scheduledAction.isCancelled = true
        }
    }

    @MainActor
    func runScheduledActions() {
        let actions = scheduledActions
        scheduledActions.removeAll()

        for scheduledAction in actions where !scheduledAction.isCancelled {
            scheduledAction.action()
        }
    }
}

private final class TestOrchestratorSettingsStore: AppSettingsStoreType {
    var storedSettings = AppSettings.default

    func load() -> AppSettings {
        storedSettings
    }

    func save(_ settings: AppSettings) {
        storedSettings = settings
    }
}
