import XCTest

class BaseUITestCase: XCTestCase {
    let app = XCUIApplication()

    /// Element on current screen — if it's not there in 2s, the app is broken
    let defaultTimeout: TimeInterval = 2
    /// Screen transitions, tab switches
    let navigationTimeout: TimeInterval = 5
    /// Initial auth flow only (cold start)
    let loginTimeout: TimeInterval = 15

    var includeResetStateLaunchArgument: Bool { true }
    /// Override to `true` in tests that need the standalone login screen
    /// (skips onboarding). Default is `false` so tests that navigate from
    /// onboarding or test onboarding screens work without extra config.
    var completeOnboarding: Bool { false }
    var additionalLaunchArguments: [String] { [] }
    /// Override to `true` in suites where each test needs a clean app launch
    /// (e.g., login/onboarding tests that leave stale field text between tests).
    var relaunchBetweenTests: Bool { false }

    /// Tracks whether the app has been launched for the current test suite.
    /// Reset once per suite via `class setUp()`, so the first test in each
    /// suite gets a fresh app launch while subsequent tests reuse the session.
    private static var hasLaunchedForCurrentSuite = false

    override class func setUp() {
        super.setUp()
        hasLaunchedForCurrentSuite = false
    }

    override func setUpWithError() throws {
        continueAfterFailure = false
        XCUIDevice.shared.orientation = .portrait

        // Auto-dismiss any system alerts (notifications, tracking, etc.)
        addUIInterruptionMonitor(withDescription: "System Alert") { alert in
            let buttons = ["Allow", "OK", "Don't Allow", "Not Now", "Dismiss", "Allow While Using App"]
            for label in buttons {
                let button = alert.buttons[label]
                if button.exists {
                    button.tap()
                    return true
                }
            }
            return false
        }

        var launchArguments = [
            "--ui-testing",
            "--disable-animations"
        ]
        if completeOnboarding {
            launchArguments.append("--complete-onboarding")
        }
        if includeResetStateLaunchArgument {
            launchArguments.append("--reset-state")
        }
        launchArguments.append(contentsOf: additionalLaunchArguments)
        app.launchArguments = launchArguments

        // First test in each suite always gets a clean app launch (handles parallel clone reuse).
        // Subsequent tests reuse the running app unless relaunchBetweenTests is true.
        let needsLaunch = !Self.hasLaunchedForCurrentSuite
            || relaunchBetweenTests
            || app.state != .runningForeground

        if needsLaunch {
            if app.state == .runningForeground {
                app.terminate()
            }
            app.launch()
            app.otherElements["ui.app.ready"].waitForExistenceOrFail(timeout: loginTimeout)
            Self.hasLaunchedForCurrentSuite = true
        }
    }

    override func tearDownWithError() throws {
        if let run = testRun, !run.hasSucceeded {
            let attachment = XCTAttachment(screenshot: app.screenshot())
            attachment.name = "Failure-\(name)"
            attachment.lifetime = .keepAlways
            add(attachment)
        }
    }
}

extension XCUIElement {
    @discardableResult
    func waitForExistenceOrFail(
        timeout: TimeInterval,
        message: String? = nil,
        file: StaticString = #filePath,
        line: UInt = #line
    ) -> XCUIElement {
        if !waitForExistence(timeout: timeout) {
            XCTFail(message ?? "Expected element to exist: \(self)", file: file, line: line)
        }
        return self
    }

    @discardableResult
    func waitUntilHittable(
        timeout: TimeInterval,
        message: String? = nil,
        file: StaticString = #filePath,
        line: UInt = #line
    ) -> XCUIElement {
        let predicate = NSPredicate(format: "exists == true AND hittable == true")
        let expectation = XCTNSPredicateExpectation(predicate: predicate, object: self)
        let result = XCTWaiter().wait(for: [expectation], timeout: timeout)

        if result != .completed {
            XCTFail(message ?? "Expected element to become hittable: \(self)", file: file, line: line)
        }

        return self
    }

    @discardableResult
    func waitForNonExistence(
        timeout: TimeInterval,
        message: String? = nil,
        file: StaticString = #filePath,
        line: UInt = #line
    ) -> Bool {
        let predicate = NSPredicate(format: "exists == false")
        let expectation = XCTNSPredicateExpectation(predicate: predicate, object: self)
        let result = XCTWaiter().wait(for: [expectation], timeout: timeout)

        if result != .completed {
            XCTFail(message ?? "Expected element to disappear: \(self)", file: file, line: line)
            return false
        }

        return true
    }

    func scrollIntoView(
        in scrollView: XCUIElement,
        maxSwipes: Int = 8,
        file: StaticString = #filePath,
        line: UInt = #line
    ) {
        if isHittable { return }

        for _ in 0..<maxSwipes {
            scrollView.swipeUp()
            if isHittable { return }
        }

        for _ in 0..<maxSwipes {
            scrollView.swipeDown()
            if isHittable { return }
        }

        XCTFail("Failed to scroll element into view: \(self)", file: file, line: line)
    }

    func forceTap(file: StaticString = #filePath, line: UInt = #line) {
        guard exists else {
            XCTFail("Element does not exist for tap: \(self)", file: file, line: line)
            return
        }
        tap()
    }

    /// Robustly acquires keyboard focus and types text into a field.
    ///
    /// Taps the field and verifies focus before typing. For SecureTextFields
    /// where `hasKeyboardFocus` is unreliable, types directly after tapping.
    ///
    /// Strategy (in order):
    /// 1. Tap element directly (if hittable) or via coordinate
    /// 2. Retry with offset coordinate tap
    /// 3. Dismiss keyboard, scroll field into view, then retry
    /// 4. Final fallback: forceTap + app.typeText
    /// Tap a text field and type text. No retries. No coordinate taps. Fail fast.
    func focusAndType(
        _ text: String,
        app: XCUIApplication,
        file: StaticString = #filePath,
        line: UInt = #line
    ) {
        guard exists else {
            XCTFail("Element does not exist: \(self)", file: file, line: line)
            return
        }

        // SecureTextFields may trigger iOS strong password suggestion dialog
        // which blocks the regular keyboard. Handle them with a dedicated path.
        if elementType == .secureTextField {
            // Dismiss any open keyboard first — iOS 26 fails to transfer focus
            // from a TextField to a SecureTextField if the keyboard is already up.
            if app.keyboards.firstMatch.exists {
                let navBar = app.navigationBars.firstMatch
                if navBar.exists {
                    navBar.tap()
                }
                _ = app.keyboards.firstMatch.waitForNonExistence(timeout: 2)
            }

            tap()
            // Dismiss "Choose My Own Password" or "Not Now" if iOS suggests a strong password
            let chooseOwn = app.buttons["Choose My Own Password"]
            if chooseOwn.waitForExistence(timeout: 0.5) {
                chooseOwn.tap()
            } else {
                let notNow = app.buttons["Not Now"]
                if notNow.exists && notNow.isHittable { notNow.tap() }
            }
            // Wait for keyboard after tapping SecureTextField
            if !app.keyboards.firstMatch.waitForExistence(timeout: 5) {
                // Retry tap — first tap may not have acquired focus
                tap()
                _ = app.keyboards.firstMatch.waitForExistence(timeout: 3)
            }
            if app.keyboards.firstMatch.exists {
                typeText(text)
            } else {
                XCTFail("Keyboard did not appear after tapping SecureTextField: \(self)", file: file, line: line)
            }
            return
        }

        // If keyboard is already open (from previous field), dismiss it
        // by tapping the navigation bar (a neutral area that won't trigger onSubmit)
        if app.keyboards.firstMatch.exists {
            let navBar = app.navigationBars.firstMatch
            if navBar.exists {
                navBar.tap()
            }
            _ = app.keyboards.firstMatch.waitForNonExistence(timeout: 2)
        }

        // Tap the element — XCUIElement.tap() uses accessibility, not coordinates
        tap()

        // Wait for keyboard — proof that this field got focus
        guard app.keyboards.firstMatch.waitForExistence(timeout: 2) else {
            XCTFail("Keyboard did not appear after tapping: \(self)", file: file, line: line)
            return
        }

        // typeText on element — if it fails (email keyboard type bug), use app.typeText
        // Since we dismissed the keyboard before tapping, app.typeText targets the correct field
        if hasKeyboardFocus {
            typeText(text)
        } else {
            app.typeText(text)
        }
    }

    /// Selects all text in a text field and types replacement text.
    ///
    /// Uses long-press to invoke the editing menu, taps "Select All", then
    /// types `newText` which overwrites the selection. This is far more
    /// reliable than pressing the delete key repeatedly.
    func clearAndEnterText(
        _ newText: String,
        app: XCUIApplication,
        file: StaticString = #filePath,
        line: UInt = #line
    ) {
        guard exists else {
            XCTFail("Element does not exist for clearAndEnterText: \(self)", file: file, line: line)
            return
        }

        // Dismiss any open keyboard first so this field isn't blocked.
        // KeyboardDismisser tries a toolbar Done + tap-above strategy before
        // falling back to the Return key — this avoids scroll-to-visible
        // errors when the keyboard is mid-transition.
        if app.keyboards.firstMatch.exists {
            KeyboardDismisser.dismiss(app: app)
        }

        // Wait for the element to be hittable (form may need to adjust after keyboard dismiss)
        let hittablePred = NSPredicate(format: "isHittable == true")
        let hittableExp = XCTNSPredicateExpectation(predicate: hittablePred, object: self)
        _ = XCTWaiter().wait(for: [hittableExp], timeout: 5)

        // Tap to focus
        tap()

        guard app.keyboards.firstMatch.waitForExistence(timeout: 2) else {
            XCTFail("Keyboard did not appear for clearAndEnterText: \(self)", file: file, line: line)
            return
        }

        // Select all text: long-press → Select All, or triple-tap
        press(forDuration: 1.0)
        let selectAll = app.menuItems["Select All"]
        if selectAll.waitForExistence(timeout: 2) {
            selectAll.tap()
        } else {
            tap(withNumberOfTaps: 3, numberOfTouches: 1)
        }

        // Type replacement (replaces selection)
        if hasKeyboardFocus {
            typeText(newText)
        } else {
            app.typeText(newText)
        }
    }
}
