import XCTest

/// Base class for all page objects providing common waiting and assertion utilities.
///
/// Replaces ad-hoc `sleep()` calls with condition-based waits for reliable,
/// non-flaky UI tests. All screen page objects should inherit from this class.
class BaseScreen {
    let app: XCUIApplication
    let timeout: TimeInterval

    init(app: XCUIApplication, timeout: TimeInterval = 10) {
        self.app = app
        self.timeout = timeout
    }

    // MARK: - Wait Helpers (replaces fixed sleeps)

    /// Waits for an element to exist within the timeout period.
    /// Fails the test with a descriptive message if the element does not appear.
    @discardableResult
    func waitForElement(_ element: XCUIElement, timeout: TimeInterval? = nil) -> XCUIElement {
        let t = timeout ?? self.timeout
        XCTAssertTrue(element.waitForExistence(timeout: t), "Element \(element) did not appear within \(t)s")
        return element
    }

    /// Waits for an element to disappear within the timeout period.
    /// Fails the test if the element is still present after the timeout.
    func waitForElementToDisappear(_ element: XCUIElement, timeout: TimeInterval? = nil) {
        let t = timeout ?? self.timeout
        let predicate = NSPredicate(format: "exists == false")
        let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
        let result = XCTWaiter().wait(for: [expectation], timeout: t)
        XCTAssertEqual(result, .completed, "Element \(element) did not disappear within \(t)s")
    }

    /// Waits for an element to become hittable (visible and interactable).
    /// Returns the element for chaining.
    @discardableResult
    func waitForHittable(_ element: XCUIElement, timeout: TimeInterval? = nil) -> XCUIElement {
        let t = timeout ?? self.timeout
        let predicate = NSPredicate(format: "isHittable == true")
        let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
        _ = XCTWaiter().wait(for: [expectation], timeout: t)
        return element
    }

    /// Waits until a condition evaluates to true, polling every 0.5s.
    /// More flexible than element-based waits for complex state checks.
    func waitForCondition(
        _ description: String,
        timeout: TimeInterval? = nil,
        condition: () -> Bool
    ) -> Bool {
        let t = timeout ?? self.timeout
        let deadline = Date().addingTimeInterval(t)
        while Date() < deadline {
            if condition() { return true }
            RunLoop.current.run(until: Date().addingTimeInterval(0.5))
        }
        return false
    }

    /// Waits for an element to exist, then taps it. Convenience for the common wait+tap pattern.
    @discardableResult
    func tapElement(_ element: XCUIElement, timeout: TimeInterval? = nil) -> XCUIElement {
        waitForElement(element, timeout: timeout)
        element.tap()
        return element
    }

    // MARK: - State Assertions

    /// Asserts that an element with the given accessibility identifier exists.
    func assertExists(_ identifier: String, file: StaticString = #file, line: UInt = #line) {
        let element = app.descendants(matching: .any)[identifier]
        XCTAssertTrue(element.waitForExistence(timeout: timeout), "Element '\(identifier)' not found", file: file, line: line)
    }

    /// Asserts that an element with the given accessibility identifier does not exist.
    func assertNotExists(_ identifier: String, file: StaticString = #file, line: UInt = #line) {
        let element = app.descendants(matching: .any)[identifier]
        XCTAssertFalse(element.exists, "Element '\(identifier)' should not exist", file: file, line: line)
    }

    // MARK: - Navigation

    /// Taps the first button in the navigation bar (typically the back button).
    func tapBackButton() {
        app.navigationBars.buttons.element(boundBy: 0).tap()
    }

    /// Subclasses must override this property to indicate whether the screen is currently displayed.
    var isDisplayed: Bool {
        fatalError("Subclasses must override isDisplayed")
    }
}
