import XCTest

/// Suite 11 — captures the gitea#2 regression at the user-visible level:
/// after onboarding (register → name residence → bulk-create tasks → land
/// on home), tapping the residence cell shows "no tasks" even though the
/// server has them. Restarting the app fixes it. This test reproduces the
/// flow without an app restart and asserts that tasks render on the
/// residence detail screen.
///
/// CRITICAL: this test must FAIL at the cache-unification fix's first
/// commit and must PASS after Phase 1-3 lands. The failing assertion is
/// pinned to a specific message so the regression is unambiguous.
///
/// The test deliberately does NOT visit the Tasks tab between onboarding
/// and tapping the residence cell. Visiting the Tasks tab would prime
/// `_allTasks` and mask the bug — the bug is that residence detail
/// cannot recover from the empty-cache + sink-timing window on its own.
final class Suite11_TaskCacheRegressionTests: BaseUITestCase {
    // We need to start at the onboarding welcome screen, not the standalone
    // login screen — `completeOnboarding` would skip the entire flow.
    override var completeOnboarding: Bool { false }
    // Single test in this suite — relaunch isn't necessary, but we want a
    // clean state every time (handled by the default --reset-state).
    override var relaunchBetweenTests: Bool { true }

    // MARK: - Constants

    /// DEBUG_FIXED_CODES=true on the local Go API hardcodes this code.
    private let debugVerificationCode = "123456"

    /// Stable name for the residence we create in onboarding. Used both for
    /// the form input and to address the cell on the home screen via
    /// `app.staticTexts[residenceName]` if the id-based identifier doesn't
    /// resolve in time.
    private let residenceName = "UI Test Property"

    // MARK: - Test

    /// Reproduces gitea#2: tasks created via the onboarding bulk endpoint
    /// must appear on the residence detail screen without an app restart
    /// and without first visiting the Tasks tab.
    @MainActor
    func test_tasksAppearOnResidenceDetail_afterOnboarding_withoutRestart() throws {
        // Step 1 — Register a fresh user via the onboarding Start Fresh flow.
        // The flow is: Welcome → ValueProps → NameResidence → CreateAccount
        //              → VerifyEmail → HomeProfile → FirstTask → main app.
        let createAccount = TestFlows.navigateStartFreshToCreateAccount(
            app: app,
            residenceName: residenceName
        )
        createAccount.waitForLoad(timeout: navigationTimeout)

        // Step 2 — Fill the create-account form. We address the onboarding
        // form's fields (not the standalone register sheet's fields).
        let creds = TestAccountManager.uniqueCredentials(prefix: "gitea2")

        createAccount.expandEmailSignup()

        // Use the same focusAndType path that OnboardingTests uses — it
        // already handles SecureTextField + iOS strong-password panel.
        // Under --ui-testing, OrganicOnboardingSecureField defaults to
        // visibility=ON (renders as TextField) to dodge the iOS 26 SecureField
        // keyboard bug. Query textFields, not secureTextFields.
        let usernameField = app.textFields[AccessibilityIdentifiers.Onboarding.usernameField]
        let emailField = app.textFields[AccessibilityIdentifiers.Onboarding.emailField]
        let passwordField = app.textFields[AccessibilityIdentifiers.Onboarding.passwordField]
        let confirmPasswordField = app.textFields[AccessibilityIdentifiers.Onboarding.confirmPasswordField]

        usernameField.waitForExistenceOrFail(timeout: navigationTimeout)
        usernameField.focusAndType(creds.username, app: app)
        emailField.waitForExistenceOrFail(timeout: navigationTimeout)
        emailField.focusAndType(creds.email, app: app)
        passwordField.waitForExistenceOrFail(timeout: navigationTimeout)
        passwordField.focusAndType(creds.password, app: app)
        confirmPasswordField.waitForExistenceOrFail(timeout: navigationTimeout)
        confirmPasswordField.focusAndType(creds.password, app: app)

        let createAccountButton = app.descendants(matching: .any)
            .matching(identifier: AccessibilityIdentifiers.Onboarding.createAccountButton)
            .firstMatch
        createAccountButton.waitForExistenceOrFail(timeout: navigationTimeout)
        createAccountButton.forceTap()

        // Step 3 — Verify email with the debug fixed code.
        let verification = VerificationScreen(app: app)
        verification.waitForLoad(timeout: loginTimeout)
        verification.enterCode(debugVerificationCode)
        // Many onboarding verification screens auto-submit on a 6-digit
        // code. If a verify button still exists and a code field is still
        // visible, tap it to push past edge cases.
        if verification.codeField.waitForExistence(timeout: 1) && verification.verifyButton.exists {
            verification.submitCode()
        }

        // Step 4 — Skip the home-profile step. The home-profile screen has
        // its own Skip button (the shared onboarding skip in the nav bar)
        // which routes to the first-task step without making us pick climate
        // / appliance fields.
        let onboardingSkipButton = app.buttons[AccessibilityIdentifiers.Onboarding.skipButton]
        XCTAssertTrue(
            onboardingSkipButton.waitForExistence(timeout: loginTimeout),
            "Onboarding skip button should exist on the home-profile screen"
        )
        // The skip button can briefly be non-hittable during the screen-in
        // transition. Use forceTap() to bypass the strict hittable check.
        // We confirmed existence above; if the tap doesn't land on the
        // intended button the next assertion (Browse All tab) will catch it.
        onboardingSkipButton.forceTap()

        // Step 5 — Switch to the "Browse All" tab on the First-Task screen.
        // "For You" suggestions can be empty for a fresh residence with no
        // home-profile data, so deterministic browsing is required.
        // The tab bar is a SwiftUI segmented Picker — its segments are
        // exposed as buttons with the segment label, regardless of an
        // identifier on the parent.
        let browseAllTab = app.buttons["Browse All"]
        XCTAssertTrue(
            browseAllTab.waitForExistence(timeout: loginTimeout),
            "Browse All tab should appear on the first-task screen"
        )
        browseAllTab.tap()

        // Step 6 — Pick 3 templates by accessibility identifier prefix.
        // The catalog is loaded via GET /api/tasks/templates/grouped/, so
        // we need to wait for at least one row to render before tapping.
        let templateRowQuery = app.buttons.matching(
            NSPredicate(format: "identifier BEGINSWITH %@",
                        AccessibilityIdentifiers.Onboarding.templateRowPrefix)
        )

        // Wait for the catalog to load. The grouped endpoint returns first
        // category expanded by default in the view, so rows should appear
        // shortly after Browse All becomes visible. Network call: 10s.
        let firstRow = templateRowQuery.element(boundBy: 0)
        XCTAssertTrue(
            firstRow.waitForExistence(timeout: loginTimeout),
            "At least one template row must render on the Browse All tab. " +
            "If no rows appear, the catalog endpoint failed — bug repro is invalid."
        )

        // Tap the first 3 visible rows. Some categories may collapse rows
        // we never see; we only need at least 1, so the floor is 1 with a
        // soft cap of 3.
        let rowCount = templateRowQuery.count
        let toPick = min(3, rowCount)
        XCTAssertGreaterThanOrEqual(toPick, 1, "Expected at least one template row")
        for index in 0..<toPick {
            let row = templateRowQuery.element(boundBy: index)
            row.waitUntilHittable(timeout: navigationTimeout)
            row.tap()
        }

        // Step 7 — Submit the bulk-create. This is the
        // POST /api/tasks/bulk/ call that produces the inconsistent client
        // cache state at the heart of gitea#2.
        let submitButton = app.buttons[AccessibilityIdentifiers.Onboarding.submitTasksButton]
        XCTAssertTrue(
            submitButton.waitForExistence(timeout: navigationTimeout),
            "Submit-tasks button must exist on the first-task screen"
        )
        submitButton.waitUntilHittable(timeout: navigationTimeout).tap()

        // Step 8 — Land on the main app (Residences tab is selected by
        // default). CRITICAL: do NOT tap the Tasks tab. Tapping it would
        // populate `_allTasks` and mask the bug.
        let mainTabs = app.otherElements[UITestID.Root.mainTabs]
        let tabBar = app.tabBars.firstMatch
        let reachedMain = mainTabs.waitForExistence(timeout: loginTimeout)
            || tabBar.waitForExistence(timeout: navigationTimeout)
        XCTAssertTrue(reachedMain, "App should reach main tabs after onboarding submit")

        // Step 9 — Tap the residence cell directly. Prefer the
        // identifier-prefix match for any cell; fall back to the static
        // text match by name.
        let residenceCellQuery = app.buttons.matching(
            NSPredicate(format: "identifier BEGINSWITH %@",
                        AccessibilityIdentifiers.Residence.cellPrefix)
        )
        let residenceCell = residenceCellQuery.firstMatch
        if residenceCell.waitForExistence(timeout: navigationTimeout) && residenceCell.isHittable {
            residenceCell.tap()
        } else {
            // Fallback: tap the static text inside the card. The
            // NavigationLink wraps the entire card so a tap on the name
            // still routes into the detail view.
            let residenceText = app.staticTexts[residenceName]
            XCTAssertTrue(
                residenceText.waitForExistence(timeout: navigationTimeout),
                "Residence cell or name '\(residenceName)' must exist on the residences list"
            )
            residenceText.tap()
        }

        // Step 10 — THE BUG ASSERTION. With the bug present:
        //   - `_allTasks` is null on the client (never primed).
        //   - `_tasksByResidence[id]` is empty (cache miss).
        //   - residence detail attempts to load, hits the iOS Combine sink
        //     timing window, and renders the empty state.
        // With the fix, both `_allTasks` is populated by `bulkCreateTasks`
        // and residence detail filters from it in-memory, so the empty
        // state must not appear.
        let taskRowQuery = app.descendants(matching: .any).matching(
            NSPredicate(format: "identifier BEGINSWITH %@",
                        AccessibilityIdentifiers.Task.rowPrefix)
        )
        let firstTaskRow = taskRowQuery.element(boundBy: 0)
        let anyTaskAppeared = firstTaskRow.waitForExistence(timeout: 10)

        let emptyState = app.otherElements[AccessibilityIdentifiers.Task.noTasksLabel]
        let emptyStateVisible = emptyState.exists

        // Pin the failure message so the bug-capture is unambiguous. This
        // is the assertion that should FAIL at this commit and PASS after
        // the cache fix lands. Don't change the message — Task 12 grep's
        // for it.
        XCTAssertTrue(
            anyTaskAppeared && !emptyStateVisible,
            "Tasks created during onboarding must appear on residence detail without restart (gitea#2)"
        )
    }
}
