import Foundation
import Testing
import UIKit
@testable import Oppi

@Suite("Tool presentation configuration")
@MainActor
struct ToolPresentationConfigTests {
    @Test func inlineMediaWarningHeuristicDoesNotFlagParityTools() {
        let sample = "let sample = \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUg==\""

        #expect(
            !ToolPresentationBuilder.shouldWarnInlineMediaForToolOutput(
                normalizedTool: "read",
                outputPreview: sample,
                fullOutput: ""
            )
        )
        #expect(
            !ToolPresentationBuilder.shouldWarnInlineMediaForToolOutput(
                normalizedTool: "functions.read",
                outputPreview: "",
                fullOutput: sample
            )
        )
        #expect(
            !ToolPresentationBuilder.shouldWarnInlineMediaForToolOutput(
                normalizedTool: "write",
                outputPreview: sample,
                fullOutput: ""
            )
        )
        #expect(
            !ToolPresentationBuilder.shouldWarnInlineMediaForToolOutput(
                normalizedTool: "tools/write",
                outputPreview: "",
                fullOutput: sample
            )
        )
        #expect(
            !ToolPresentationBuilder.shouldWarnInlineMediaForToolOutput(
                normalizedTool: "edit",
                outputPreview: sample,
                fullOutput: ""
            )
        )
    }

    @Test func inlineMediaWarningHeuristicKeepsBashPlainText() {
        #expect(
            !ToolPresentationBuilder.shouldWarnInlineMediaForToolOutput(
                normalizedTool: "bash",
                outputPreview: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg==",
                fullOutput: ""
            )
        )

        #expect(
            !ToolPresentationBuilder.shouldWarnInlineMediaForToolOutput(
                normalizedTool: "functions.bash",
                outputPreview: "",
                fullOutput: "before data:audio/wav;base64,UklGRg== after"
            )
        )

        #expect(
            !ToolPresentationBuilder.shouldWarnInlineMediaForToolOutput(
                normalizedTool: "bash\n",
                outputPreview: "plain output",
                fullOutput: ""
            )
        )
    }

    @Test func inlineMediaWarningHeuristicDetectsDataURIsForNonBashTools() {
        #expect(
            ToolPresentationBuilder.shouldWarnInlineMediaForToolOutput(
                normalizedTool: "grep",
                outputPreview: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg==",
                fullOutput: ""
            )
        )

        #expect(
            ToolPresentationBuilder.shouldWarnInlineMediaForToolOutput(
                normalizedTool: "find",
                outputPreview: "",
                fullOutput: "before data:audio/wav;base64,UklGRg== after"
            )
        )
    }

    @Test func inlineMediaWarningHeuristicFlagsUnknownExtensionToolsButNotBuiltins() {
        let sample = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg=="

        #expect(
            ToolPresentationBuilder.shouldWarnInlineMediaForToolOutput(
                normalizedTool: "notes",
                outputPreview: sample,
                fullOutput: ""
            )
        )
        #expect(
            ToolPresentationBuilder.shouldWarnInlineMediaForToolOutput(
                normalizedTool: "lookup",
                outputPreview: sample,
                fullOutput: ""
            )
        )
        #expect(
            !ToolPresentationBuilder.shouldWarnInlineMediaForToolOutput(
                normalizedTool: "edit",
                outputPreview: sample,
                fullOutput: ""
            )
        )
    }

    @Test func inlineMediaWarningHeuristicIsCaseInsensitiveForUnknownTools() {
        #expect(
            ToolPresentationBuilder.shouldWarnInlineMediaForToolOutput(
                normalizedTool: "grep",
                outputPreview: "DATA:IMAGE/PNG;BASE64,iVBORw0KGgoAAAANSUhEUg==",
                fullOutput: ""
            )
        )

        #expect(
            ToolPresentationBuilder.shouldWarnInlineMediaForToolOutput(
                normalizedTool: "grep",
                outputPreview: "",
                fullOutput: "before DaTa:AuDiO/WaV;BaSe64,UklGRg== after"
            )
        )
    }

    @Test func inlineMediaToolsStayNativeAndSurfaceWarningBadge() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        let item = ChatItem.toolCall(
            id: "grep-media-1",
            tool: "grep",
            argsSummary: "pattern: data",
            outputPreview: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg==",
            outputByteCount: 128,
            isError: false,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: item.id, item: item))
        #expect(config.languageBadge == "⚠︎media")
    }

    @Test func collapsedParityToolsUseNativeToolConfiguration() {
        let harness = makeTimelineHarness(sessionId: "session-a")
        seedCollapsedParityToolArgs(in: harness)

        for row in collapsedParityToolRows {
            #expect(harness.coordinator.toolRowConfiguration(itemID: row.id, item: row) != nil)
        }
    }

    @Test func toolRowsRenderWithNativeToolConfigurationAcrossStates() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")

        harness.toolArgsStore.set(["path": .string("src/main.swift")], for: "read-1")
        harness.toolArgsStore.set(["path": .string("src/main.swift")], for: "write-1")
        harness.toolArgsStore.set([
            "path": .string("src/main.swift"),
            "edits": .array([
                .object([
                    "oldText": .string("let value = 1\n"),
                    "newText": .string("let value = 2\n"),
                ]),
            ]),
        ], for: "edit-1")
        harness.toolArgsStore.set(["command": .string("echo hi")], for: "bash-1")

        let rows: [ChatItem] = [
            .toolCall(id: "bash-1", tool: "bash", argsSummary: "command: echo hi", outputPreview: "", outputByteCount: 0, isError: false, isDone: false),
            .toolCall(id: "read-1", tool: "read", argsSummary: "path: src/main.swift", outputPreview: "line1\nline2", outputByteCount: 32, isError: false, isDone: true),
            .toolCall(id: "write-1", tool: "write", argsSummary: "path: src/main.swift", outputPreview: "", outputByteCount: 16, isError: false, isDone: true),
            .toolCall(id: "edit-1", tool: "edit", argsSummary: "path: src/main.swift", outputPreview: "", outputByteCount: 16, isError: false, isDone: true),
            .toolCall(id: "extension-1", tool: "extensions.backlog", argsSummary: "action: list-all", outputPreview: "", outputByteCount: 0, isError: false, isDone: true),
        ]

        harness.applyAndLayout(items: rows)

        try expectTimelineRowsUseConfigurationType(
            in: harness.collectionView,
            items: Array(rows.indices),
            as: ToolTimelineRowConfiguration.self
        )
    }

    @Test func editToolWithoutDiffArgsUsesModifiedTrailingFallback() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        let item = ChatItem.toolCall(
            id: "edit-unknown-diff",
            tool: "edit",
            argsSummary: "path: src/main.swift",
            outputPreview: "",
            outputByteCount: 0,
            isError: false,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: item.id, item: item))
        #expect(config.editAdded == nil)
        #expect(config.editRemoved == nil)
        #expect(config.trailing == "modified")
    }

    @Test func expandedEditToolUsesNativeDiffLines() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        harness.reducer.expandedItemIDs.insert("edit-diff")
        harness.toolArgsStore.set([
            "edits": .array([
                .object([
                    "oldText": .string("let value = 1\nlet unchanged = true\n"),
                    "newText": .string("let value = 2\nlet unchanged = true\nlet added = true\n"),
                ]),
            ]),
            "path": .string("src/main.swift"),
        ], for: "edit-diff")

        let item = ChatItem.toolCall(
            id: "edit-diff",
            tool: "edit",
            argsSummary: "path: src/main.swift",
            outputPreview: "",
            outputByteCount: 0,
            isError: false,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: item.id, item: item))
        guard case .diff(let diffLines, _) = config.expandedContent else { Issue.record("Expected .diff"); return }

        let stats = diffLines.reduce(into: (added: 0, removed: 0)) { acc, line in
            switch line.kind {
            case .added:
                acc.added += 1
            case .removed:
                acc.removed += 1
            case .context:
                break
            }
        }

        #expect(stats.added > 0)
        #expect(stats.removed > 0)
        // expandedText == nil is now implicit (content is .diff not .text)
    }

    @Test func expandedStreamingEditUsesCheapFileSurfaceUntilDone() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        harness.reducer.expandedItemIDs.insert("edit-streaming")
        harness.toolArgsStore.set([
            "edits": .array([
                .object([
                    "oldText": .string("let value = 1\n"),
                    "newText": .string("let value = 2\nlet added = true\n"),
                ]),
            ]),
            "path": .string("src/main.swift"),
        ], for: "edit-streaming")

        let item = ChatItem.toolCall(
            id: "edit-streaming",
            tool: "edit",
            argsSummary: "path: src/main.swift",
            outputPreview: "",
            outputByteCount: 0,
            isError: false,
            isDone: false
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: item.id, item: item))
        guard case .code(let text, let language, let startLine, let filePath) = config.expandedContent else {
            Issue.record("Expected streaming edit to stay on cheap .code surface")
            return
        }
        #expect(text == "let value = 2\nlet added = true\n")
        #expect(language == .swift)
        #expect(startLine == 1)
        #expect(filePath == "src/main.swift")
        #expect(config.copyOutputText == "let value = 2\nlet added = true\n")
    }

    @Test func expandedEditToolFallbackOutputKeepsSyntaxLanguageFromPath() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        harness.reducer.expandedItemIDs.insert("edit-fallback")
        harness.toolArgsStore.set([
            "path": .string("src/feature.ts"),
        ], for: "edit-fallback")

        let item = ChatItem.toolCall(
            id: "edit-fallback",
            tool: "edit",
            argsSummary: "path: src/feature.ts",
            outputPreview: "const value = 1",
            outputByteCount: 16,
            isError: true,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: item.id, item: item))
        // Error edit falls back to code viewer with language from file path
        guard case .code(_, let language, _, let filePath) = config.expandedContent else { Issue.record("Expected .code for error edit fallback"); return }
        #expect(language == .typescript)
        #expect(filePath == "src/feature.ts")
    }

    @Test func expandedEditFallbackKeepsCodeRendererForMarkdownPaths() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        harness.reducer.expandedItemIDs.insert("edit-md-fallback")
        harness.toolArgsStore.set([
            "path": .string("docs/README.md"),
        ], for: "edit-md-fallback")

        let item = ChatItem.toolCall(
            id: "edit-md-fallback",
            tool: "edit",
            argsSummary: "path: docs/README.md",
            outputPreview: "# Title",
            outputByteCount: 7,
            isError: true,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: item.id, item: item))
        guard case .code(let text, let language, _, let filePath) = config.expandedContent else {
            Issue.record("Expected .code for edit markdown fallback")
            return
        }
        #expect(text == "# Title")
        #expect(language == nil)
        #expect(filePath == "docs/README.md")
    }

    @Test func expandedReadToolDetectsSyntaxLanguageFromFilePath() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        harness.reducer.expandedItemIDs.insert("read-swift")
        harness.toolArgsStore.set([
            "path": .string("Runtime/TimelineReducer.swift"),
            "offset": .number(270),
            "limit": .number(60),
        ], for: "read-swift")

        let item = ChatItem.toolCall(
            id: "read-swift",
            tool: "read",
            argsSummary: "path: Runtime/TimelineReducer.swift",
            outputPreview: "guard value else { return }",
            outputByteCount: 42,
            isError: false,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: item.id, item: item))
        guard case .code(_, let language, _, _) = config.expandedContent else { Issue.record("Expected .code"); return }
        #expect(language == .swift)
        #expect(config.languageBadge == "Swift")
        // expandedText != nil is now implicit in content case
        if case .code(_, _, let startLine, _) = config.expandedContent { #expect(startLine == 270) }
        if case .code(_, _, _, let filePath) = config.expandedContent { #expect(filePath == "Runtime/TimelineReducer.swift") }
    }

    @Test func expandedReadToolFallsBackToArgsSummaryPathWhenArgsMissing() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        harness.reducer.expandedItemIDs.insert("read-fallback")

        let item = ChatItem.toolCall(
            id: "read-fallback",
            tool: "read",
            argsSummary: "path: Sources/Agent.swift",
            outputPreview: "let value = 1",
            outputByteCount: 12,
            isError: false,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: item.id, item: item))
        if case .code(_, _, let startLine, _) = config.expandedContent { #expect(startLine == 1) }
        if case .code(_, _, _, let filePath) = config.expandedContent { #expect(filePath == "Sources/Agent.swift") }
    }

    @Test func collapsedReadImageToolStaysHeaderOnly() throws {
        // End-to-end: image reads should collapse like normal file reads.
        // The image is available after expansion, but the collapsed row stays compact.
        let harness = makeTimelineHarness(sessionId: "session-a")
        let fakeBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg=="
        let serverOutput = "Read image file [image/png]\ndata:image/png;base64,\(fakeBase64)"

        harness.toolArgsStore.set(["path": .string("icon-design/pi-icon.png")], for: "read-img")
        harness.toolOutputStore.append(serverOutput, to: "read-img")

        let item = ChatItem.toolCall(
            id: "read-img",
            tool: "read",
            argsSummary: "path: icon-design/pi-icon.png",
            outputPreview: String(serverOutput.prefix(500)),
            outputByteCount: serverOutput.utf8.count,
            isError: false,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: item.id, item: item))
        #expect(config.collapsedImageBase64 == nil)
        #expect(config.collapsedImageMimeType == nil)
        #expect(config.languageBadge == FileType.image.displayLabel)
    }

    @Test func collapsedReadImageToolWithEmptyOutputHasNoPreview() throws {
        // When the tool output hasn't arrived yet (streaming), no image preview.
        let harness = makeTimelineHarness(sessionId: "session-a")
        harness.toolArgsStore.set(["path": .string("icon.png")], for: "read-img-empty")

        let item = ChatItem.toolCall(
            id: "read-img-empty",
            tool: "read",
            argsSummary: "path: icon.png",
            outputPreview: "",
            outputByteCount: 0,
            isError: false,
            isDone: false
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: item.id, item: item))
        #expect(config.collapsedImageBase64 == nil, "No image preview before output arrives")
    }

    @Test func expandedWriteToolDetectsSyntaxLanguageFromPath() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        harness.reducer.expandedItemIDs.insert("write-swift")
        harness.toolArgsStore.set([
            "path": .string("Sources/Generated.swift"),
        ], for: "write-swift")

        let item = ChatItem.toolCall(
            id: "write-swift",
            tool: "write",
            argsSummary: "path: Sources/Generated.swift",
            outputPreview: "struct Generated {}",
            outputByteCount: 20,
            isError: false,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: item.id, item: item))
        // Write without content arg falls back to code viewer with language from file path
        guard case .code(_, let language, _, let filePath) = config.expandedContent else { Issue.record("Expected .code for write fallback"); return }
        #expect(language == .swift)
        #expect(filePath == "Sources/Generated.swift")
    }

    @Test func expandedWriteMarkdownUsesMarkdownRendererLikeRead() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        harness.reducer.expandedItemIDs.insert("write-md")
        harness.toolArgsStore.set([
            "path": .string("docs/README.md"),
            "content": .string("# Title\n\nBody"),
        ], for: "write-md")

        let item = ChatItem.toolCall(
            id: "write-md",
            tool: "write",
            argsSummary: "path: docs/README.md",
            outputPreview: "",
            outputByteCount: 14,
            isError: false,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: item.id, item: item))
        guard case .markdown(let text) = config.expandedContent else { Issue.record("Expected .markdown for write file content"); return }
        #expect(text == "# Title\n\nBody")
        #expect(config.languageBadge == "Markdown")
    }

    @Test func expandedStreamingWriteMarkdownUsesIncrementalMarkdownPipeline() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        harness.reducer.expandedItemIDs.insert("write-md-streaming")
        harness.toolArgsStore.set([
            "path": .string("docs/README.md"),
            "content": .string("# Title\n\nBody"),
        ], for: "write-md-streaming")

        let item = ChatItem.toolCall(
            id: "write-md-streaming",
            tool: "write",
            argsSummary: "path: docs/README.md",
            outputPreview: "",
            outputByteCount: 14,
            isError: false,
            isDone: false
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: item.id, item: item))
        guard case .markdown(let text) = config.expandedContent else {
            Issue.record("Expected streaming markdown write to use .markdown (incremental pipeline), got \(String(describing: config.expandedContent))")
            return
        }
        #expect(text == "# Title\n\nBody")
        #expect(config.languageBadge == "Markdown")
    }

    @Test func expandedReadMarkdownUsesMarkdownRendererAndSkipsCodeLineNumbers() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        harness.reducer.expandedItemIDs.insert("read-md")
        harness.toolArgsStore.set([
            "path": .string("docs/README.md"),
            "offset": .number(1),
            "limit": .number(80),
        ], for: "read-md")

        let item = ChatItem.toolCall(
            id: "read-md",
            tool: "read",
            argsSummary: "path: docs/README.md",
            outputPreview: "# Title",
            outputByteCount: 80,
            isError: false,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: item.id, item: item))
        guard case .markdown = config.expandedContent else { Issue.record("Expected .markdown"); return }
        // startLine is implicit in content case
        #expect(config.languageBadge == "Markdown")
    }

    @Test func expandedParityToolsUseNativeToolConfiguration() {
        let harness = makeTimelineHarness(sessionId: "session-a")

        let rows: [ChatItem] = [
            .toolCall(id: "read-1", tool: "read", argsSummary: "", outputPreview: "", outputByteCount: 0, isError: false, isDone: true),
            .toolCall(id: "read-2", tool: "functions.read", argsSummary: "", outputPreview: "", outputByteCount: 0, isError: false, isDone: true),
            .toolCall(id: "write-1", tool: "write", argsSummary: "", outputPreview: "", outputByteCount: 0, isError: false, isDone: true),
            .toolCall(id: "edit-1", tool: "edit", argsSummary: "", outputPreview: "", outputByteCount: 0, isError: false, isDone: true),
            .toolCall(id: "extension-1", tool: "extensions.backlog", argsSummary: "", outputPreview: "", outputByteCount: 0, isError: false, isDone: true),
        ]

        for row in rows {
            harness.reducer.expandedItemIDs.insert(row.id)
            #expect(harness.coordinator.toolRowConfiguration(itemID: row.id, item: row) != nil)
        }
    }

    @Test func expandedExtensionToolAutoDetectsStructuredJSONForExpandedOutput() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        let itemID = "extension-list-1"
        harness.reducer.expandedItemIDs.insert(itemID)
        harness.toolArgsStore.set(["action": .string("list-all")], for: itemID)
        harness.toolOutputStore.append(
            """
            {
              "assigned": [
                {
                  "id": "EXT-a27df231",
                  "title": "Control tower Live Activity",
                  "status": "in_progress"
                }
              ],
              "open": [],
              "closed": []
            }
            """,
            to: itemID
        )

        let item = ChatItem.toolCall(
            id: itemID,
            tool: "extensions.backlog",
            argsSummary: "action: list-all",
            outputPreview: "",
            outputByteCount: 512,
            isError: false,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: itemID, item: item))
        guard case .text(let output, let language) = config.expandedContent else {
            Issue.record("Expected .text")
            return
        }
        #expect(output.contains("EXT-a27df231"))
        #expect(language == .json)
        #expect(config.trailing == nil)
    }

    @Test func expandedExtensionAppendUsesGenericTextOutput() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        let itemID = "extension-append-1"
        harness.reducer.expandedItemIDs.insert(itemID)
        harness.toolArgsStore.set([
            "action": .string("append"),
            "id": .string("EXT-463187a1"),
            "body": .string("Investigate smooth scroll follow\nAdd regression tests")
        ], for: itemID)
        harness.toolOutputStore.append(
            "Updated EXT-463187a1 body with 2 lines",
            to: itemID
        )

        let item = ChatItem.toolCall(
            id: itemID,
            tool: "extensions.backlog",
            argsSummary: "action: append, id: EXT-463187a1",
            outputPreview: "",
            outputByteCount: 4096,
            isError: false,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: itemID, item: item))
        #expect(config.trailing == nil)
        guard case .text(let text, let language) = config.expandedContent else {
            Issue.record("Expected .text")
            return
        }
        #expect(language == nil)
        #expect(text.contains("EXT-463187a1"))
        #expect(config.copyOutputText?.contains("EXT-463187a1") == true)
    }

    @Test func expandedExtensionUpdateUsesGenericTextOutput() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        let itemID = "extension-update-1"
        harness.reducer.expandedItemIDs.insert(itemID)
        harness.toolArgsStore.set([
            "action": .string("update"),
            "id": .string("EXT-463187a1"),
            "status": .string("closed"),
            "title": .string("Refine auto-follow scrolling during streaming"),
            "body": .string("Done.\nValidated on simulator and device.")
        ], for: itemID)
        harness.toolOutputStore.append(
            "Updated EXT-463187a1 (status: closed)",
            to: itemID
        )

        let item = ChatItem.toolCall(
            id: itemID,
            tool: "extensions.backlog",
            argsSummary: "action: update, id: EXT-463187a1",
            outputPreview: "",
            outputByteCount: 2048,
            isError: false,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: itemID, item: item))
        #expect(config.trailing == nil)
        guard case .text(let text, let language) = config.expandedContent else {
            Issue.record("Expected .text")
            return
        }
        #expect(language == nil)
        #expect(text.contains("status: closed"))
        #expect(config.copyOutputText?.contains("status: closed") == true)
    }

    @Test func expandedFileAndDiffToolsRenderViaNativePathWithoutFailsafe() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        ChatTimelinePerf.reset()

        harness.reducer.expandedItemIDs.insert("read-tool-1")
        harness.reducer.expandedItemIDs.insert("edit-tool-1")

        let rows: [ChatItem] = [
            .toolCall(id: "read-tool-1", tool: "read", argsSummary: "path: src/main.swift", outputPreview: "", outputByteCount: 64, isError: false, isDone: true),
            .toolCall(id: "edit-tool-1", tool: "edit", argsSummary: "path: src/main.swift", outputPreview: "", outputByteCount: 128, isError: false, isDone: true),
        ]

        harness.applyAndLayout(items: rows)

        try expectTimelineRowsUseConfigurationType(
            in: harness.collectionView,
            items: Array(rows.indices),
            as: ToolTimelineRowConfiguration.self
        )

        let snapshot = ChatTimelinePerf.snapshot()
        #expect(snapshot.failsafeConfigureCount == 0)
    }

    @Test func collapsedParityToolsRenderViaNativeShellWithoutFailsafe() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        ChatTimelinePerf.reset()
        seedCollapsedParityToolArgs(in: harness)

        harness.applyAndLayout(items: collapsedParityToolRows)

        try expectTimelineRowsUseConfigurationType(
            in: harness.collectionView,
            items: Array(collapsedParityToolRows.indices),
            as: ToolTimelineRowConfiguration.self
        )

        let snapshot = ChatTimelinePerf.snapshot()
        #expect(snapshot.failsafeConfigureCount == 0)
    }

    @Test func nativeBashToolConfigurationOmitsCollapsedOutputPreview() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        let item = ChatItem.toolCall(
            id: "tool-1",
            tool: "bash",
            argsSummary: "command: echo hi",
            outputPreview: "hi",
            outputByteCount: 32,
            isError: false,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: "tool-1", item: item))

        #expect(config.preview == nil)
        #expect(config.trailing == nil)
        #expect(config.toolNamePrefix == "$")
        #expect(!config.title.hasPrefix("$"))
    }

    @Test func nativeReadToolConfigurationUsesSingleLineHeaderAndHidesCollapsedByteCount() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        harness.toolArgsStore.set(["path": .string("src/main.swift")], for: "read-1")

        let item = ChatItem.toolCall(
            id: "read-1",
            tool: "read",
            argsSummary: "path: src/main.swift",
            outputPreview: "line1\nline2",
            outputByteCount: 0,
            isError: false,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: "read-1", item: item))
        #expect(config.preview == nil)
        #expect(config.trailing == nil)
    }

    @Test func expandedReadImageToolConfigurationUsesMediaRenderer() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        harness.reducer.expandedItemIDs.insert("read-image-1")
        harness.toolArgsStore.set(["path": .string("screens/harness-initial.png")], for: "read-image-1")
        harness.toolOutputStore.append(
            "Read image file [image/png]\ndata:image/png;base64,iVBORw0KGgoAAAANSUhEUg==",
            to: "read-image-1"
        )

        let item = ChatItem.toolCall(
            id: "read-image-1",
            tool: "read",
            argsSummary: "path: screens/harness-initial.png",
            outputPreview: "",
            outputByteCount: 128,
            isError: false,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: "read-image-1", item: item))
        guard case .readMedia = config.expandedContent else { Issue.record("Expected .readMedia"); return }
        // startLine is implicit in content case
    }

    @Test func collapsedToolRowsHideByteCountTrailingByDefault() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        harness.toolArgsStore.set(["path": .string("src/main.swift")], for: "read-1")

        let bash = ChatItem.toolCall(
            id: "bash-1",
            tool: "bash",
            argsSummary: "command: ls",
            outputPreview: "line",
            outputByteCount: 1024,
            isError: false,
            isDone: true
        )
        let read = ChatItem.toolCall(
            id: "read-1",
            tool: "read",
            argsSummary: "path: src/main.swift",
            outputPreview: "line",
            outputByteCount: 2048,
            isError: false,
            isDone: true
        )
        let write = ChatItem.toolCall(
            id: "write-1",
            tool: "write",
            argsSummary: "path: src/main.swift",
            outputPreview: "ok",
            outputByteCount: 4096,
            isError: false,
            isDone: true
        )

        let bashConfig = try #require(harness.coordinator.toolRowConfiguration(itemID: "bash-1", item: bash))
        let readConfig = try #require(harness.coordinator.toolRowConfiguration(itemID: "read-1", item: read))
        let writeConfig = try #require(harness.coordinator.toolRowConfiguration(itemID: "write-1", item: write))

        #expect(bashConfig.trailing == nil)
        #expect(readConfig.trailing == nil)
        #expect(writeConfig.trailing == nil)
    }

    @Test func collapsedExtensionToolConfigurationOmitsPreviewForSingleLineConsistency() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        let toolID = "extension-preview-1"

        harness.toolArgsStore.set([
            "action": .string("append"),
            "title": .string("Refine timeline behavior"),
            "body": .string("First line\nSecond line"),
        ], for: toolID)

        let item = ChatItem.toolCall(
            id: toolID,
            tool: "extensions.backlog",
            argsSummary: "action: append",
            outputPreview: "",
            outputByteCount: 0,
            isError: false,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: toolID, item: item))
        #expect(config.preview == nil)
    }

    @Test func readOutputFileTypeDetectsFromRawSummaryWithLineRange() {
        let fileType = ToolPresentationBuilder.readOutputFileType(
            args: nil,
            argsSummary: "Chat/ChatTimelineCollectionView.swift:440-499"
        )

        #expect(fileType == .code(language: .swift))
    }

    @Test func nativeReadToolConfigurationInfersLanguageBadgeFromRawSummaryWhenArgsMissing() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")

        let item = ChatItem.toolCall(
            id: "read-summary-only",
            tool: "read",
            argsSummary: "Chat/ChatTimelineCollectionView.swift:440-499",
            outputPreview: "data:image/png literal inside source",
            outputByteCount: 2048,
            isError: false,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: item.id, item: item))
        #expect(config.languageBadge == "Swift")
    }

    @Test func expandedBashToolConfigurationPrefersUnwrappedOutput() throws {
        let harness = makeTimelineHarness(sessionId: "session-a")
        harness.reducer.expandedItemIDs.insert("bash-1")

        let item = ChatItem.toolCall(
            id: "bash-1",
            tool: "bash",
            argsSummary: "command: tail -16 build.log",
            outputPreview: "line",
            outputByteCount: 10,
            isError: false,
            isDone: true
        )

        let config = try #require(harness.coordinator.toolRowConfiguration(itemID: "bash-1", item: item))
        guard case .bash = config.expandedContent else { Issue.record("Expected .bash"); return }
        if case .bash(_, _, let unwrapped) = config.expandedContent { #expect(unwrapped) }
        #expect(config.toolNamePrefix == "$")
        #expect(config.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
    }

}

private let collapsedParityToolRows: [ChatItem] = [
    .toolCall(
        id: "parity-read-1",
        tool: "read",
        argsSummary: "path: src/main.swift",
        outputPreview: "line1\nline2",
        outputByteCount: 64,
        isError: false,
        isDone: true
    ),
    .toolCall(
        id: "parity-read-2",
        tool: "functions.read",
        argsSummary: "path: src/main.swift",
        outputPreview: "line1\nline2",
        outputByteCount: 64,
        isError: false,
        isDone: true
    ),
    .toolCall(
        id: "parity-write-1",
        tool: "write",
        argsSummary: "path: src/main.swift",
        outputPreview: "",
        outputByteCount: 128,
        isError: false,
        isDone: true
    ),
    .toolCall(
        id: "parity-edit-1",
        tool: "edit",
        argsSummary: "path: src/main.swift",
        outputPreview: "",
        outputByteCount: 128,
        isError: false,
        isDone: true
    ),
    .toolCall(
        id: "parity-extension-1",
        tool: "extensions.backlog",
        argsSummary: "action: list-all",
        outputPreview: "",
        outputByteCount: 0,
        isError: false,
        isDone: true
    ),
]

@MainActor
private func seedCollapsedParityToolArgs(in harness: TimelineTestHarness) {
    harness.toolArgsStore.set(["path": .string("src/main.swift")], for: "parity-read-1")
    harness.toolArgsStore.set(["path": .string("src/main.swift")], for: "parity-read-2")
    harness.toolArgsStore.set(["path": .string("src/main.swift")], for: "parity-write-1")
    harness.toolArgsStore.set([
        "path": .string("src/main.swift"),
        "edits": .array([
            .object([
                "oldText": .string("let value = 1\n"),
                "newText": .string("let value = 2\n"),
            ]),
        ]),
    ], for: "parity-edit-1")
}
