//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the container project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//

import XCTest

@testable import TerminalProgress

final class ProgressBarTests: XCTestCase {
    func testSpinner() async throws {
        let config = try ProgressConfig(
            description: "Task"
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task [0s]")
    }

    func testSpinnerFinished() async throws {
        let config = try ProgressConfig(
            description: "Task"
        )
        let progress = ProgressBar(config: config)
        progress.finish()
        let output = progress.draw()
        XCTAssertEqual(output, "✔ Task [0s]")
    }

    func testNoSpinner() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSpinner: false
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertEqual(output, "Task [0s]")
    }

    func testNoSpinnerFinished() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSpinner: false
        )
        let progress = ProgressBar(config: config)
        progress.finish()
        let output = progress.draw()
        XCTAssertEqual(output, "Task [0s]")
    }

    func testNoTasks() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showTasks: false
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task [0s]")
    }

    func testTasks() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showTasks: true
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task [0s]")
    }

    func testTasksAdd() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showTasks: true
        )
        let progress = ProgressBar(config: config)
        progress.add(tasks: 1)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task [0s]")
    }

    func testTasksSet() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showTasks: true
        )
        let progress = ProgressBar(config: config)
        progress.set(tasks: 2)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task [0s]")
    }

    func testTotalTasks() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showTasks: true,
            totalTasks: 2
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ [0/2] Task [0s]")
    }

    func testTotalTasksFinished() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showTasks: true,
            totalTasks: 2
        )
        let progress = ProgressBar(config: config)
        progress.finish()
        let output = progress.draw()
        XCTAssertEqual(output, "✔ [0/2] Task [0s]")
    }

    func testTotalTasksAdd() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showTasks: true,
            totalTasks: 1
        )
        let progress = ProgressBar(config: config)
        progress.add(totalTasks: 1)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ [0/2] Task [0s]")
    }

    func testTotalTasksSet() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showTasks: true,
            totalTasks: 1
        )
        let progress = ProgressBar(config: config)
        progress.set(totalTasks: 2)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ [0/2] Task [0s]")
    }

    func testTotalTasksInvalid() throws {
        do {
            let _ = try ProgressConfig(description: "test", totalTasks: 0)
        } catch ProgressConfig.Error.invalid(_) {
            return
        }
        XCTFail("expected ProgressConfig.Error.invalid")
    }

    func testDescription() async throws {
        let config = try ProgressConfig(
            description: "Task"
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task [0s]")
    }

    func testNoDescription() async throws {
        let config = try ProgressConfig()
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ [0s]")
    }

    func testNoPercent() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showPercent: false,
            totalItems: 2
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task [0s]")
    }

    func testPercentHidden() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showPercent: true
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task [0s]")
    }

    func testPercentItems() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showPercent: true,
            totalItems: 2
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task 50% [0s]")
    }

    func testPercentItemsFinished() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showPercent: true,
            totalItems: 2
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        progress.finish()
        let output = progress.draw()
        XCTAssertEqual(output, "✔ Task 100% [0s]")
    }

    func testPercentSize() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showPercent: true,
            showSize: false,
            showSpeed: false,
            totalSize: 2
        )
        let progress = ProgressBar(config: config)
        progress.set(size: 1)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task 50% [0s]")
    }

    func testPercentSizeFinished() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showPercent: true,
            showSize: false,
            showSpeed: false,
            totalSize: 2
        )
        let progress = ProgressBar(config: config)
        progress.set(size: 1)
        progress.finish()
        let output = progress.draw()
        XCTAssertEqual(output, "✔ Task 100% [0s]")
    }

    func testNoProgressBar() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showProgressBar: false,
            totalItems: 2,
            width: 57
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task 50% [0s]")
    }

    func testProgressBar() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showProgressBar: true,
            totalItems: 2,
            width: 57
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        let output = progress.draw()
        XCTAssertEqual(output, "Task 50% |██  | [0s]")
    }

    func testProgressBarFinished() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showProgressBar: true,
            totalItems: 2,
            width: 57
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        progress.finish()
        let output = progress.draw()
        XCTAssertEqual(output, "Task 100% |███| [0s]")
    }

    func testProgressBarMinWidth() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showProgressBar: true,
            totalItems: 2,
            width: 13
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        let output = progress.draw()
        XCTAssertEqual(output, "Task 50% | | [0s]")
    }

    func testProgressBarMinWidthFinished() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showProgressBar: true,
            totalItems: 2,
            width: 13
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        progress.finish()
        let output = progress.draw()
        XCTAssertEqual(output, "Task 100% |█| [0s]")
    }

    func testNoItems() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showItems: false
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task [0s]")
    }

    func testItemsZero() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showItems: true
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task [0s]")
    }

    func testItemsAdd() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showItems: true
        )
        let progress = ProgressBar(config: config)
        progress.add(items: 1)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task (1 it) [0s]")
    }

    func testItemsAddFinish() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showItems: true
        )
        let progress = ProgressBar(config: config)
        progress.add(items: 1)
        progress.finish()
        let output = progress.draw()
        XCTAssertEqual(output, "✔ Task [0s]")
    }

    func testItemsSet() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showItems: true
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 2)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task (2 it) [0s]")
    }

    func testTotalItemsZeroItems() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showItems: true,
            totalItems: 1
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task 0% [0s]")
    }

    func testTotalItems() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showItems: true,
            totalItems: 2
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task 50% (1 of 2 it) [0s]")
    }

    func testTotalItemsFinish() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showItems: true,
            totalItems: 2
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        progress.finish()
        let output = progress.draw()
        XCTAssertEqual(output, "✔ Task 100% (2 it) [0s]")
    }

    func testTotalItemsAdd() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showItems: true,
            totalItems: 1
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        progress.add(totalItems: 1)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task 50% (1 of 2 it) [0s]")
    }

    func testTotalItemsSet() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showItems: true,
            totalItems: 1
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        progress.set(totalItems: 2)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task 50% (1 of 2 it) [0s]")
    }

    func testTotalItemsInvalid() throws {
        do {
            let _ = try ProgressConfig(description: "test", totalItems: 0)
        } catch ProgressConfig.Error.invalid(_) {
            return
        }
        XCTFail("expected ProgressConfig.Error.invalid")
    }

    func testNoSize() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSize: false
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task [0s]")
    }

    func testSizeZero() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSize: true
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task [0s]")
    }

    func testSizeAdd() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSize: true,
            showSpeed: false
        )
        let progress = ProgressBar(config: config)
        progress.add(size: 1)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task (1 byte) [0s]")
    }

    func testSizeAddFinish() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSize: true,
            showSpeed: false
        )
        let progress = ProgressBar(config: config)
        progress.add(size: 1)
        progress.finish()
        let output = progress.draw()
        XCTAssertEqual(output, "✔ Task [0s]")
    }

    func testSizeSet() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSize: true,
            showSpeed: false
        )
        let progress = ProgressBar(config: config)
        progress.set(size: 2)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task (2 bytes) [0s]")
    }

    func testTotalSizeZeroSize() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSize: true,
            totalSize: 1
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task 0% [0s]")
    }

    func testTotalSizeDifferentUnits() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSize: true,
            showSpeed: false,
            totalSize: 2
        )
        let progress = ProgressBar(config: config)
        progress.set(size: 1)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task 50% (1 byte/2 bytes) [0s]")
    }

    func testTotalSizeDifferentUnitsFinish() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSize: true,
            showSpeed: false,
            totalSize: 2
        )
        let progress = ProgressBar(config: config)
        progress.set(size: 1)
        progress.finish()
        let output = progress.draw()
        XCTAssertEqual(output, "✔ Task 100% (2 bytes) [0s]")
    }

    func testTotalSizeSameUnits() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSize: true,
            showSpeed: false,
            totalSize: 4
        )
        let progress = ProgressBar(config: config)
        progress.set(size: 2)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task 50% (2/4 bytes) [0s]")
    }

    func testTotalSizeSameUnitsFinish() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSize: true,
            showSpeed: false,
            totalSize: 4
        )
        let progress = ProgressBar(config: config)
        progress.set(size: 2)
        progress.finish()
        let output = progress.draw()
        XCTAssertEqual(output, "✔ Task 100% (4 bytes) [0s]")
    }

    func testTotalSizeAdd() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSize: true,
            showSpeed: false,
            totalSize: 3
        )
        let progress = ProgressBar(config: config)
        progress.set(size: 2)
        progress.add(totalSize: 1)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task 50% (2/4 bytes) [0s]")
    }

    func testTotalSizeSet() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSize: true,
            showSpeed: false,
            totalSize: 3
        )
        let progress = ProgressBar(config: config)
        progress.set(size: 2)
        progress.set(totalSize: 4)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task 50% (2/4 bytes) [0s]")
    }

    func testTotalSizeInvalid() throws {
        do {
            let _ = try ProgressConfig(description: "test", totalSize: 0)
        } catch ProgressConfig.Error.invalid(_) {
            return
        }
        XCTFail("expected ProgressConfig.Error.invalid")
    }

    func testItemsAndSize() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showItems: true,
            showSize: true,
            showSpeed: false,
            totalItems: 2,
            totalSize: 4
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        progress.set(size: 2)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task 50% (1 of 2 it, 2/4 bytes) [0s]")
    }

    func testItemsAndSizeFinish() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showItems: true,
            showSize: true,
            showSpeed: false,
            totalItems: 2,
            totalSize: 4
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        progress.set(size: 2)
        progress.finish()
        let output = progress.draw()
        XCTAssertEqual(output, "✔ Task 100% (2 it, 4 bytes) [0s]")
    }

    func testNoSpeed() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSpeed: false,
            totalSize: 4
        )
        let progress = ProgressBar(config: config)
        progress.set(size: 2)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task 50% (2/4 bytes) [0s]")
    }

    func testSpeed() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSpeed: true,
            totalSize: 4
        )
        let progress = ProgressBar(config: config)
        progress.set(size: 2)
        let output = progress.draw()
        XCTAssertTrue(output.contains("/s"))
    }

    func testSpeedFinish() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSpeed: true,
            totalSize: 4
        )
        let progress = ProgressBar(config: config)
        progress.set(size: 2)
        progress.finish()
        let output = progress.draw()
        XCTAssertFalse(output.contains("/s"))
    }

    func testItemsSizeAndSpeed() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showItems: true,
            showSize: true,
            showSpeed: true,
            totalItems: 2,
            totalSize: 4
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        progress.set(size: 2)
        let output = progress.draw()
        XCTAssertTrue(output.contains("1 of 2 it, 2/4 bytes"))
        XCTAssertTrue(output.contains("/s"))
    }

    func testItemsSizeAndSpeedFinish() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showItems: true,
            showSize: true,
            showSpeed: true,
            totalItems: 2,
            totalSize: 4
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        progress.set(size: 2)
        progress.finish()
        let output = progress.draw()
        XCTAssertTrue(output.contains("2 it, 4 bytes"))
        XCTAssertFalse(output.contains("/s"))
    }

    func testNoTime() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showTime: false
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task")
    }

    func testTime() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showTime: true
        )
        let progress = ProgressBar(config: config)
        sleep(1)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task [1s]")
    }

    func testIgnoreSmallSize() async throws {
        let config = try ProgressConfig(
            description: "Task",
            ignoreSmallSize: true,
            totalSize: 4
        )
        let progress = ProgressBar(config: config)
        progress.set(size: 2)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task [0s]")
    }

    func testProgressBarSizeExceedsTotal() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showProgressBar: true,
            totalSize: 50
        )
        let progress = ProgressBar(config: config)
        progress.set(size: 100)
        let _ = progress.draw()
    }

    func testItemsName() async throws {
        let config = try ProgressConfig(
            description: "Task",
            itemsName: "files",
            showItems: true,
            totalItems: 2
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        let output = progress.draw()
        XCTAssertEqual(output, "⠋ Task 50% (1 of 2 files) [0s]")
    }

    func testPlainModeConfig() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSpinner: false,
            outputMode: .plain
        )
        XCTAssertEqual(config.outputMode, .plain)
    }

    func testPlainModeDraw() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSpinner: false,
            outputMode: .plain
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertEqual(output, "Task [0s]")
    }

    func testPlainModeDrawWithTasks() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSpinner: false,
            showTasks: true,
            totalTasks: 2,
            outputMode: .plain
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertEqual(output, "[0/2] Task [0s]")
    }

    func testPlainModeDrawWithPercent() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSpinner: false,
            showItems: true,
            totalItems: 2,
            outputMode: .plain
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        let output = progress.draw()
        XCTAssertEqual(output, "Task 50% (1 of 2 it) [0s]")
    }

    func testPlainModeFinished() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSpinner: false,
            showTasks: true,
            totalTasks: 2,
            clearOnFinish: false,
            outputMode: .plain
        )
        let progress = ProgressBar(config: config)
        progress.set(tasks: 2)
        progress.finish()
        let output = progress.draw()
        XCTAssertEqual(output, "[2/2] Task [0s]")
    }

    func testPlainModeWithSize() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSpinner: false,
            showSize: true,
            showSpeed: false,
            totalSize: 4,
            outputMode: .plain
        )
        let progress = ProgressBar(config: config)
        progress.set(size: 2)
        let output = progress.draw()
        XCTAssertEqual(output, "Task 50% (2/4 bytes) [0s]")
    }

    func testPlainModeNoProgressBar() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSpinner: false,
            showProgressBar: false,
            totalItems: 2,
            outputMode: .plain
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        let output = progress.draw()
        XCTAssertEqual(output, "Task 50% [0s]")
    }

    func testPlainModeNoAnsiEscapes() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSpinner: false,
            outputMode: .plain
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertFalse(output.contains("\u{001B}"))
    }

    func testPlainModeTerminalOutput() async throws {
        let pipe = Pipe()
        let config = try ProgressConfig(
            terminal: pipe.fileHandleForWriting,
            description: "Task",
            showSpinner: false,
            clearOnFinish: false,
            outputMode: .plain
        )
        let progress = ProgressBar(config: config)
        progress.render(force: true)
        progress.finish()
        try pipe.fileHandleForWriting.close()

        let data = pipe.fileHandleForReading.readDataToEndOfFile()
        let output = String(decoding: data, as: UTF8.self)
        let lines = output.components(separatedBy: "\n").filter { !$0.isEmpty }
        // Expect exactly 2 lines: one from render, one from finish
        XCTAssertEqual(lines.count, 2)
        XCTAssertTrue(lines[0].contains("Task"))
        XCTAssertTrue(lines[1].contains("Task"))
    }

    func testPlainModeTerminalOutputNoAnsiEscapes() async throws {
        let pipe = Pipe()
        let config = try ProgressConfig(
            terminal: pipe.fileHandleForWriting,
            description: "Task",
            showSpinner: false,
            clearOnFinish: false,
            outputMode: .plain
        )
        let progress = ProgressBar(config: config)
        progress.render(force: true)
        progress.finish()
        try pipe.fileHandleForWriting.close()

        let data = pipe.fileHandleForReading.readDataToEndOfFile()
        let output = String(decoding: data, as: UTF8.self)
        XCTAssertFalse(output.contains("\u{001B}"))
    }

    func testPlainModeTerminalOutputUsesNewlines() async throws {
        let pipe = Pipe()
        let config = try ProgressConfig(
            terminal: pipe.fileHandleForWriting,
            description: "Task",
            showSpinner: false,
            clearOnFinish: false,
            outputMode: .plain
        )
        let progress = ProgressBar(config: config)
        progress.render(force: true)
        progress.finish()
        try pipe.fileHandleForWriting.close()

        let data = pipe.fileHandleForReading.readDataToEndOfFile()
        let output = String(decoding: data, as: UTF8.self)
        // Plain mode should use newlines, not carriage returns
        XCTAssertFalse(output.contains("\r"))
        XCTAssertTrue(output.contains("\n"))
    }

    func testPlainModeDefaultClearOnFinishOmitsFinalLine() async throws {
        let pipe = Pipe()
        let config = try ProgressConfig(
            terminal: pipe.fileHandleForWriting,
            description: "Task",
            showSpinner: false,
            outputMode: .plain
        )
        let progress = ProgressBar(config: config)
        progress.render(force: true)
        progress.finish()
        try pipe.fileHandleForWriting.close()

        let data = pipe.fileHandleForReading.readDataToEndOfFile()
        let output = String(decoding: data, as: UTF8.self)
        let lines = output.components(separatedBy: "\n").filter { !$0.isEmpty }
        XCTAssertEqual(lines.count, 1)
        XCTAssertEqual(lines.first, "Task [0s]")
    }

    func testOutputModeDefaultIsAnsi() async throws {
        let config = try ProgressConfig(description: "Task")
        XCTAssertEqual(config.outputMode, .ansi)
    }

    // MARK: - Color mode tests

    func testColorModeConfig() async throws {
        let config = try ProgressConfig(
            description: "Task",
            outputMode: .color
        )
        XCTAssertEqual(config.outputMode, .color)
    }

    func testVisibleLengthPlainText() async throws {
        let text = "hello"
        XCTAssertEqual(text.visibleLength, 5)
    }

    func testVisibleLengthWithAnsiCodes() async throws {
        let text = "\u{001B}[36mhello\u{001B}[0m"
        XCTAssertEqual(text.visibleLength, 5)
    }

    func testVisibleLengthEmptyString() async throws {
        XCTAssertEqual("".visibleLength, 0)
    }

    func testColorModeDraw() async throws {
        let config = try ProgressConfig(
            description: "Task",
            outputMode: .color
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        let stripped = output.replacingOccurrences(
            of: "\u{001B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression
        )
        XCTAssertEqual(stripped, "⠋ Task [0s]")
    }

    func testColorModeDrawContainsAnsiCodes() async throws {
        let config = try ProgressConfig(
            description: "Task",
            outputMode: .color
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertTrue(output.contains("\u{001B}["))
        XCTAssertTrue(output.contains(EscapeSequence.cyan))  // spinner
        XCTAssertTrue(output.contains(EscapeSequence.bold))  // description
        XCTAssertTrue(output.contains(EscapeSequence.dim))  // time
        XCTAssertTrue(output.contains(EscapeSequence.reset))  // reset
    }

    func testColorModeDrawFinished() async throws {
        let config = try ProgressConfig(
            description: "Task",
            outputMode: .color
        )
        let progress = ProgressBar(config: config)
        progress.finish()
        let output = progress.draw()
        let stripped = output.replacingOccurrences(
            of: "\u{001B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression
        )
        XCTAssertEqual(stripped, "✔ Task [0s]")
        XCTAssertTrue(output.contains(EscapeSequence.green))  // done icon
    }

    func testColorModeDrawWithTasks() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showTasks: true,
            totalTasks: 2,
            outputMode: .color
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        let stripped = output.replacingOccurrences(
            of: "\u{001B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression
        )
        XCTAssertEqual(stripped, "⠋ [0/2] Task [0s]")
        XCTAssertTrue(output.contains(EscapeSequence.cyan))  // tasks
    }

    func testColorModeDrawWithPercent() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showItems: true,
            totalItems: 2,
            outputMode: .color
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        let output = progress.draw()
        let stripped = output.replacingOccurrences(
            of: "\u{001B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression
        )
        XCTAssertEqual(stripped, "⠋ Task 50% (1 of 2 it) [0s]")
        XCTAssertTrue(output.contains(EscapeSequence.yellow))  // in-progress percent
    }

    func testColorModeDrawWithPercentFinished() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showItems: true,
            totalItems: 2,
            outputMode: .color
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        progress.finish()
        let output = progress.draw()
        let stripped = output.replacingOccurrences(
            of: "\u{001B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression
        )
        XCTAssertEqual(stripped, "✔ Task 100% (2 it) [0s]")
        XCTAssertTrue(output.contains(EscapeSequence.green))  // finished percent
    }

    func testColorModeDrawWithSize() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showSize: true,
            showSpeed: false,
            totalSize: 4,
            outputMode: .color
        )
        let progress = ProgressBar(config: config)
        progress.set(size: 2)
        let output = progress.draw()
        let stripped = output.replacingOccurrences(
            of: "\u{001B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression
        )
        XCTAssertEqual(stripped, "⠋ Task 50% (2/4 bytes) [0s]")
        XCTAssertTrue(output.contains(EscapeSequence.dim))  // parens content
    }

    func testColorModeDrawVisibleLengthMatchesContent() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showTasks: true,
            showItems: true,
            totalTasks: 2,
            totalItems: 4,
            outputMode: .color
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 2)
        let colorOutput = progress.draw()

        let plainConfig = try ProgressConfig(
            description: "Task",
            showTasks: true,
            showItems: true,
            totalTasks: 2,
            totalItems: 4
        )
        let plainProgress = ProgressBar(config: plainConfig)
        plainProgress.set(items: 2)
        let plainOutput = plainProgress.draw()

        XCTAssertEqual(colorOutput.visibleLength, plainOutput.count)
    }

    func testColorModeNoAnsiCodesInContent() async throws {
        let config = try ProgressConfig(
            description: "Task",
            outputMode: .color
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        let stripped = output.replacingOccurrences(
            of: "\u{001B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression
        )
        XCTAssertFalse(stripped.contains("\u{001B}"))
    }

    func testColorModeDrawWithProgressBar() async throws {
        let config = try ProgressConfig(
            description: "Task",
            showProgressBar: true,
            totalItems: 2,
            width: 57,
            outputMode: .color
        )
        let progress = ProgressBar(config: config)
        progress.set(items: 1)
        let output = progress.draw()
        let stripped = output.replacingOccurrences(
            of: "\u{001B}\\[[0-9;]*[a-zA-Z]", with: "", options: .regularExpression
        )
        XCTAssertEqual(stripped, "Task 50% |██  | [0s]")
        XCTAssertTrue(output.contains(EscapeSequence.green))
    }

    func testColorModeRequiresTTY() async throws {
        let pipe = Pipe()
        let config = try ProgressConfig(
            terminal: pipe.fileHandleForWriting,
            description: "Task",
            outputMode: .color
        )
        let progress = ProgressBar(config: config)
        // Pipe is not a TTY, so render should produce no output (same as ansi)
        progress.render(force: true)
        progress.finish()
        try pipe.fileHandleForWriting.close()

        let data = pipe.fileHandleForReading.readDataToEndOfFile()
        XCTAssertTrue(data.isEmpty)
    }

    func testColorModeDrawOutputContainsColorCodes() async throws {
        // Verify draw() includes ANSI codes even without a TTY
        // (draw is separate from terminal rendering)
        let config = try ProgressConfig(
            description: "Task",
            outputMode: .color
        )
        let progress = ProgressBar(config: config)
        let output = progress.draw()
        XCTAssertTrue(output.contains(EscapeSequence.cyan))
        XCTAssertTrue(output.contains(EscapeSequence.bold))
        XCTAssertTrue(output.contains(EscapeSequence.dim))
        XCTAssertTrue(output.contains(EscapeSequence.reset))
    }

    func testColorModeRenderTerminatorIsCarriageReturn() async throws {
        // Color mode is TTY-only so we cannot capture its raw terminal output via a pipe.
        // Instead, verify that plain mode emits \n (newlines) while color mode shares the
        // ansi code path which emits \r (carriage returns). We confirm this by checking
        // that plain output uses \n and that color mode is distinct from plain.
        let plainPipe = Pipe()
        let plainConfig = try ProgressConfig(
            terminal: plainPipe.fileHandleForWriting,
            description: "Task",
            showSpinner: false,
            clearOnFinish: false,
            outputMode: .plain
        )
        let plainProgress = ProgressBar(config: plainConfig)
        plainProgress.render(force: true)
        plainProgress.finish()
        try plainPipe.fileHandleForWriting.close()

        let plainData = plainPipe.fileHandleForReading.readDataToEndOfFile()
        let plainOutput = String(decoding: plainData, as: UTF8.self)
        // Plain mode uses \n terminators
        XCTAssertTrue(plainOutput.contains("\n"))
        XCTAssertFalse(plainOutput.contains("\r"))

        // Color mode follows the ansi path (not plain), so it uses \r
        let colorConfig = try ProgressConfig(
            description: "Task",
            outputMode: .color
        )
        XCTAssertNotEqual(colorConfig.outputMode, .plain)
    }

    func testOutputModeDefaultIsNotColor() async throws {
        let config = try ProgressConfig(description: "Task")
        XCTAssertNotEqual(config.outputMode, .color)
        XCTAssertEqual(config.outputMode, .ansi)
    }
}
