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 claim
///
/// > A restored board that fails surfaces on welcome, row-level: its window doesn't open; welcome
/// > appears alongside whatever did restore, the failed board's recents row carrying fail-fast's
/// > specifics (load error) … never a silent drop.
///
/// 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. **No board window.** Not an empty one, not one with the good lanes in it — fail-fast is
///    all-or-nothing, so a partial board on screen would be the worse failure.
/// 2. **Welcome, loudly.** The recents row for that board wears the loader's own sentence, which
///    names the offending file. A row that fell back to "Unavailable", or to a count, would be the
///    app declining to say what it found.
/// 3. **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 — so a board it refused
///    must still be refusable, byte for byte.
///
/// ### Where each claim is checked
///
/// The first two are here, because they are about *windows* and a window is what a unit test does not
/// have. The third 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
    }

    /// The whole of claims 1 and 2, in one launch: no board window, welcome instead, and the row
    /// carrying the loader's specifics.
    @MainActor
    func testMalformedBoardFailsLoudlyAndOpensNoWindow() throws {
        let app = XCUIApplication.launched(with: .malformed)

        // Welcome is where a failed open lands (`BoardWindowHost.start`: record the failure, refresh
        // the recents, open welcome, dismiss the board window).
        XCTAssertTrue(
            app.windows["Welcome to Lanework"].waitForExistence(timeout: XCUIApplication.uiTimeout),
            "the welcome window did not appear after a failed open"
        )

        // Claim 1. Checked *after* welcome has appeared, so this is "the board window never came",
        // not "the board window has not come yet".
        XCTAssertFalse(
            app.windows[FixtureBoard.malformed.windowTitle].exists,
            "a board window opened for a board the loader rejected"
        )

        // Claim 2. The row is one combined accessibility element — name, location, caption — and the
        // caption is `BoardLoadError.description`: "⟨lane⟩/⟨card⟩/index.md: unparseable YAML at line
        // N: …". The UUIDs in that 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.element(labelContaining: "index.md").waitForExistence(timeout: XCUIApplication.uiTimeout),
            "no welcome row named the offending index.md — fail-fast's specifics did not reach the surface"
        )
        XCTAssertTrue(
            app.element(labelContaining: "unparseable YAML").exists,
            "the welcome row did not say why the board was refused"
        )
        // And it is the malformed board's own row that says it.
        XCTAssertTrue(
            app.element(labelContaining: FixtureBoard.malformed.windowTitle).exists,
            "the failure did not land on the failed board's row"
        )
    }

    /// Claim 3, 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.windows["Welcome to Lanework"].waitForExistence(timeout: XCUIApplication.uiTimeout),
            "the welcome window did not appear after a failed open"
        )

        // 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 — fail-fast must not write"
            )
        } 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.windows["Welcome to Lanework"].waitForExistence(timeout: XCUIApplication.uiTimeout),
            "the second launch did not reach welcome"
        )
        XCTAssertFalse(
            second.windows[FixtureBoard.malformed.windowTitle].exists,
            "the second launch opened the board the first one refused"
        )
        XCTAssertTrue(
            second.element(labelContaining: "index.md").waitForExistence(timeout: XCUIApplication.uiTimeout),
            "the second launch did not name the offending file"
        )
    }

    // 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
    }
}
