import XCTest

/// **A board that will not load, from the outside** (01-storage-format.md § Malformed input;
/// 02-architecture.md § Launch and window lifecycle).
///
/// ### The claims, as they stand after the decision surface
///
/// The `malformed` fixture is a well-formed board with exactly one unparseable card `index.md`
/// (`UITestLaunch.malformedIndexText` — a frontmatter flow sequence that is never closed). Building
/// it succeeds; loading it must not, and *how* it fails is the whole of this file:
///
/// 1. **The window stays, and explains itself.** The fixture opens through `AppModel.openBoard`, which
///    is an **attended** open — and 01 (settled 2026-07-31) makes an attended refusal the decision
///    surface's case: "one aggregated surface hosted by the pre-snapshot loading window — the loading
///    content transforms in place, never a sheet over a spinner". So there *is* a window, it is the
///    one that was opening, and what it shows names the offending file.
/// 2. **No board behind it.** Fail-fast is all-or-nothing: the surface is a decision, not a partial
///    board, so the good lanes are not on screen underneath it.
/// 3. **Cancel is the old landing, on demand.** "**Cancel** aborts the open: the window retires and
///    the board lands row-level on welcome, record-before-load unchanged" — which is the sequence
///    this file used to assert *unconditionally*, now reached by the user's own choice.
/// 4. **Nothing repaired.** The bytes on disk are the bytes the fixture wrote. The loader is a pure
///    function of the tree and writes nothing, ever — the Repair precedent — and the surface writes
///    only what the user chooses, which here is nothing.
///
/// **What changed and why**: claims 1 and 2 used to read "no board window, welcome instead". That was
/// the whole landing for *every* refusal before this milestone; it is now the **restored** landing
/// (`RestoreBootstrapView`) and the environmental one. The fixture launch is neither — it opens a
/// board the way a double-click does.
///
/// ### Where each claim is checked
///
/// The first three are here, because they are about *windows* and a window is what a unit test does
/// not have. The fourth is checked **both** here and in `KanbanTests` — unconditionally there
/// (`UITestMalformedFixtureBoardTests`, which builds the fixture and re-reads the tree), and
/// opportunistically here, because reaching the app's container from the runner depends on how the
/// app under test was signed and installed. Where the container is not reachable this file says so
/// and leans on the unit suite rather than inventing a pass.
final class FailFastLaunchTests: XCTestCase {

    override func setUp() {
        super.setUp()
        continueAfterFailure = false
    }

    /// Claims 1 and 2: the surface appears in the board's own window, names the file, and shows no
    /// board.
    @MainActor
    func testMalformedBoardShowsTheDecisionSurface() throws {
        let app = XCUIApplication.launched(with: .malformed)

        // The surface, by the identifier it carries for exactly this
        // (`BoardDecisionSurface.accessibilityIdentifier`).
        let surface = app.descendants(matching: .any)["decision-surface"]
        XCTAssertTrue(
            surface.waitForExistence(timeout: XCUIApplication.uiTimeout),
            "an attended open of a refusing board did not transform into the decision surface"
        )

        // It is the *opening window* that hosts it — the loading content transformed in place, so the
        // window is still called what the loading state called it.
        XCTAssertTrue(
            app.windows[FixtureBoard.malformed.windowTitle].exists,
            "the surface is not in the board's own window"
        )
        XCTAssertFalse(
            app.windows["Welcome to Lanework"].exists,
            "an attended refusal must not retire — that is the restored landing"
        )

        // The class section for unparseable frontmatter, and the specifics on its row. The UUIDs in
        // the path are minted at launch and unknowable here, so the assertion is on the parts that are
        // the *app's* to keep stable: the offending file is named, and the reason is stated.
        XCTAssertTrue(
            app.descendants(matching: .any)["decision-section-unreadable-frontmatter"].exists,
            "the defect was not grouped into its class section"
        )
        XCTAssertTrue(
            app.element(labelContaining: "index.md").exists,
            "no row named the offending index.md — fail-fast's specifics did not reach the surface"
        )
        XCTAssertTrue(
            app.element(labelContaining: "unparseable YAML").exists,
            "the row did not say what is wrong with the file"
        )

        // Claim 2: a decision, not a partial board. The fixture's intact lane is not on screen.
        XCTAssertFalse(
            app.element(labelContaining: "A good card").exists,
            "a partial board rendered behind the surface — fail-fast is all-or-nothing"
        )
    }

    /// Claim 3: Cancel is the old landing, reached by the user's own choice — the window retires and
    /// the board lands row-level on welcome with fail-fast's specifics on it.
    @MainActor
    func testCancelRetiresToWelcomeWithTheSpecifics() throws {
        let app = XCUIApplication.launched(with: .malformed)

        // Queried across every element type rather than as `app.buttons[...]`: the surface's footer
        // sits inside a `.contain` accessibility group, and which AX type a SwiftUI `Button` lands on
        // there is not something a test should be asserting about in passing.
        let cancel = app.descendants(matching: .any)["decision-cancel"]
        XCTAssertTrue(
            cancel.waitForExistence(timeout: XCUIApplication.uiTimeout),
            "the surface did not appear, so Cancel could not be pressed"
        )
        cancel.click()

        XCTAssertTrue(
            app.windows["Welcome to Lanework"].waitForExistence(timeout: XCUIApplication.uiTimeout),
            "Cancel did not land on welcome"
        )
        XCTAssertFalse(
            app.windows[FixtureBoard.malformed.windowTitle].exists,
            "the window did not retire"
        )
        // The row is one combined accessibility element — name, location, caption — and the caption is
        // `BoardLoadFailure.description`. Matched on *value* as well as label: welcome's rows are
        // list cells whose combined text lands in the AX value, not in a label (see
        // `Self.element(textContaining:in:)`).
        XCTAssertTrue(
            Self.element(textContaining: "unparseable YAML", in: app)
                .waitForExistence(timeout: XCUIApplication.uiTimeout),
            "the welcome row did not carry fail-fast's specifics"
        )
        XCTAssertTrue(
            Self.element(textContaining: FixtureBoard.malformed.windowTitle, in: app).exists,
            "the failure did not land on the failed board's row"
        )
    }

    /// The first element whose **label or value** contains `fragment`.
    ///
    /// `XCUIApplication.element(labelContaining:)` matches the label alone, which is the right question
    /// for the decision surface's own rows (each is one combined element with an explicit
    /// accessibility label) and the wrong one for welcome's recents rows: those are list cells whose
    /// name-location-caption text arrives as the cell's AX *value*.
    @MainActor
    private static func element(textContaining fragment: String, in app: XCUIApplication) -> XCUIElement {
        app.descendants(matching: .any)
            .matching(NSPredicate(format: "label CONTAINS %@ OR value CONTAINS %@", fragment, fragment))
            .firstMatch
    }

    /// Claim 4, twice over: the malformed bytes survive the refusal, and a second launch is refused
    /// the same way rather than opening a board the app quietly fixed.
    ///
    /// The relaunch is not redundant with the byte check — it is what the byte check *means* from the
    /// user's side, and it is the half that holds even where the container cannot be read.
    @MainActor
    func testMalformedBoardIsNeverRepaired() throws {
        let app = XCUIApplication.launched(with: .malformed)
        XCTAssertTrue(
            app.descendants(matching: .any)["decision-surface"].waitForExistence(timeout: XCUIApplication.uiTimeout),
            "the surface did not appear"
        )

        // The bytes, where the runner can reach them. `NSTemporaryDirectory()` inside the sandboxed
        // app resolves to its container, which an unsandboxed test runner can read — but only when
        // the app under test is installed where this path expects, so a miss is reported rather than
        // failed. `UITestMalformedFixtureBoardTests` makes the same claim unconditionally.
        if let malformed = Self.malformedIndexOnDisk() {
            XCTAssertTrue(
                malformed.contains(Self.malformationMarker),
                "the malformed index.md no longer carries its marker — something rewrote a file the loader refused to read"
            )
            XCTAssertTrue(
                malformed.contains("order: [1024"),
                "the malformed frontmatter was repaired — nothing here mints a rewrite of unparseable YAML"
            )
        } else {
            // Not a failure, and not silence either: the run says which half of the claim it made.
            XCTContext.runActivity(named: "container not reachable from the runner") { _ in
                print("""
                    The app's fixture scratch directory could not be read from the test runner, so \
                    the on-disk half of "nothing was repaired" was not checked here. It is pinned \
                    unconditionally by KanbanTests ▸ UITestMalformedFixtureBoardTests.
                    """)
            }
        }

        // The relaunch. A fresh launch rebuilds the fixture from scratch (the scratch root is wiped
        // per launch), so what this proves is the durable half: the app has no repair path that
        // would make the second attempt succeed where the first failed.
        app.terminate()
        let second = XCUIApplication.launched(with: .malformed)
        XCTAssertTrue(
            second.descendants(matching: .any)["decision-surface"].waitForExistence(timeout: XCUIApplication.uiTimeout),
            "the second launch opened the board the first one refused"
        )
        XCTAssertTrue(
            second.element(labelContaining: "unparseable YAML").exists,
            "the second launch did not name the same defect"
        )
    }

    // MARK: - Reading the app's container

    /// The string that appears only in the malformed file — `UITestLaunch.malformationMarker`,
    /// mirrored here for `FixtureBoard`'s reason.
    private static let malformationMarker = "lanework-ui-test-malformed-fixture"

    /// The malformed card's `index.md`, read from the app's sandbox container — or `nil` when the
    /// runner cannot reach it.
    ///
    /// The card folder's name is a UUID minted at launch, so the file is found by its content rather
    /// than by its path: exactly one `index.md` under the fixture board carries the marker, which is
    /// what the marker is for.
    @MainActor
    private static func malformedIndexOnDisk() -> String? {
        let home = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)
        let board = home
            .appendingPathComponent("Library/Containers/dev.rzen.indie.Kanban/Data/tmp", isDirectory: true)
            .appendingPathComponent("LaneworkUITestFixture", isDirectory: true)
            .appendingPathComponent("\(FixtureBoard.malformed.windowTitle).kanban", isDirectory: true)

        guard let walker = FileManager.default.enumerator(atPath: board.path) else { return nil }
        for case let relative as String in walker where relative.hasSuffix("index.md") {
            guard let data = try? Data(contentsOf: board.appendingPathComponent(relative)) else { continue }
            let text = String(decoding: data, as: UTF8.self)
            if text.contains(malformationMarker) { return text }
        }
        return nil
    }
}
