import Foundation
import Testing
import UIKit
@testable import Oppi

@Suite("ToolTimelineRowContentView")
struct ToolTimelineRowContentViewTests {

    @Test func mediaPlaybackTelemetryClassifiesKindWithoutContent() {
        #expect(MediaPlaybackTelemetry.mediaKind(mimeType: "video/mp4", sourceFileExtension: nil) == "video")
        #expect(MediaPlaybackTelemetry.mediaKind(mimeType: nil, sourceFileExtension: "wav") == "audio")
        #expect(MediaPlaybackTelemetry.mediaKind(mimeType: "application/octet-stream", sourceFileExtension: "PNG") == "image")
        #expect(MediaPlaybackTelemetry.mediaKind(mimeType: nil, sourceFileExtension: nil) == "unknown")
    }

    @MainActor
    @Test func emptyCollapsedBodyProducesFiniteCompactHeight() {
        let config = makeTimelineToolConfiguration(isExpanded: false)
        let view = ToolTimelineRowContentView(configuration: config)

        let size = fittedTimelineSize(for: view, width: 370)

        #expect(size.width.isFinite)
        #expect(size.height.isFinite)
        #expect(size.height > 0)
        #expect(size.height < 220)
    }

    @MainActor
    @Test func collapsedHeaderDoesNotStretchInsideTemporarilyTallCell() throws {
        let config = makeTimelineToolConfiguration(
            title: "imagen height: 432, seed: 5050, steps: 9, model: zit",
            toolNamePrefix: "imagen",
            isExpanded: false
        )
        let view = ToolTimelineRowContentView(configuration: config)
        view.frame = CGRect(x: 0, y: 0, width: 370, height: 620)

        view.setNeedsLayout()
        view.layoutIfNeeded()

        let titleLabel = try #require(timelineAllLabels(in: view).first {
            timelineRenderedText(of: $0).contains("seed: 5050")
        })
        let titleRect = titleLabel.convert(titleLabel.bounds, to: view)

        #expect(titleRect.minY < 16)
        #expect(titleRect.height < 32)
    }

    @MainActor
    @Test func collapsedTitleStaysSingleLineForConsistency() throws {
        let config = makeTimelineToolConfiguration(
            title: "extensions.backlog Refine compaction row preview behavior for consistency across timeline",
            isExpanded: false
        )
        let view = ToolTimelineRowContentView(configuration: config)

        _ = fittedTimelineSize(for: view, width: 320)

        let labels = timelineAllLabels(in: view)
        let titleLabel = try #require(labels.first {
            timelineRenderedText(of: $0).contains("extensions.backlog Refine compaction row preview behavior")
        })

        #expect(titleLabel.numberOfLines == 1)
    }

    @MainActor
    @Test func collapsedFileToolTitleUsesBreadcrumbWhenFullPathDoesNotFit() throws {
        let path = "clients/apple/Oppi/Sources/ScheduleEditView.swift"
        let config = makeTimelineToolConfiguration(
            title: path,
            toolNamePrefix: "edit",
            isExpanded: false
        )
        let view = ToolTimelineRowContentView(configuration: config)

        _ = fittedTimelineSize(for: view, width: 360)

        let labels = timelineAllLabels(in: view)
        let titleLabel = try #require(labels.first {
            timelineRenderedText(of: $0).contains("ScheduleEditView.swift")
        })

        #expect(timelineRenderedText(of: titleLabel) == "c/a/O/S/ScheduleEditView.swift")
    }

    @MainActor
    @Test func collapsedFileToolTitlePreservesFileNameBeforeFirstLayoutPass() throws {
        let path = "server/src/session-attachments.ts:1-260"
        let config = makeTimelineToolConfiguration(
            title: path,
            languageBadge: "typescript",
            titleLineBreakMode: .byTruncatingMiddle,
            toolNamePrefix: "read",
            isExpanded: false
        )
        let view = ToolTimelineRowContentView(configuration: config)

        _ = fittedTimelineSizeWithoutPrelayout(for: view, width: 360)

        let labels = timelineAllLabels(in: view)
        let titleLabel = try #require(labels.first {
            timelineRenderedText(of: $0).contains("session-attachments.ts")
        })

        #expect(timelineRenderedText(of: titleLabel) == "s/s/session-attachments.ts:1-260")
    }

    @MainActor
    @Test func collapsedEditFileToolTitleAccountsForStatsAndLanguageBadgeWidth() throws {
        let path = "server/src/cli/local-api-client.ts"
        let config = makeTimelineToolConfiguration(
            title: path,
            languageBadge: "typescript",
            titleLineBreakMode: .byTruncatingMiddle,
            toolNamePrefix: "edit",
            editAdded: 10,
            editRemoved: 4,
            isExpanded: false
        )
        let view = ToolTimelineRowContentView(configuration: config)

        _ = fittedTimelineSize(for: view, width: 360)

        let labels = timelineAllLabels(in: view)
        let titleLabel = try #require(labels.first {
            timelineRenderedText(of: $0).contains("local-api-client.ts")
        })

        #expect(timelineRenderedText(of: titleLabel) == "s/s/c/local-api-client.ts")
    }

    @MainActor
    @Test func collapsedFileToolTitleFallsBackToFileNameOnNarrowRows() throws {
        let path = "clients/apple/Oppi/Features/Chat/Support/Schedules/ScheduleEditView.swift"
        let config = makeTimelineToolConfiguration(
            title: path,
            toolNamePrefix: "write",
            isExpanded: false
        )
        let view = ToolTimelineRowContentView(configuration: config)

        _ = fittedTimelineSize(for: view, width: 260)

        let labels = timelineAllLabels(in: view)
        let titleLabel = try #require(labels.first {
            timelineRenderedText(of: $0).contains("ScheduleEditView.swift")
        })

        #expect(timelineRenderedText(of: titleLabel) == "ScheduleEditView.swift")
    }

    @MainActor
    @Test func collapsedFileToolTitleKeepsFullPathWhenItFits() throws {
        let path = "clients/apple/Oppi/Sources/ScheduleEditView.swift"
        let config = makeTimelineToolConfiguration(
            title: path,
            toolNamePrefix: "read",
            isExpanded: false
        )
        let view = ToolTimelineRowContentView(configuration: config)

        _ = fittedTimelineSize(for: view, width: 760)

        let labels = timelineAllLabels(in: view)
        let titleLabel = try #require(labels.first {
            timelineRenderedText(of: $0).contains("ScheduleEditView.swift")
        })

        #expect(timelineRenderedText(of: titleLabel) == path)
    }

    @MainActor
    @Test func expandedFileToolTitleCanWrapToShowFullPath() throws {
        let longPath = "clients/apple/Oppi/Features/Chat/Support/WorkspaceReviewFileDetailView.swift"
        let config = makeTimelineToolConfiguration(
            title: longPath,
            expandedContent: .code(
                text: "struct WorkspaceReviewFileDetailView {}",
                language: .swift,
                startLine: 1,
                filePath: longPath
            ),
            toolNamePrefix: "read",
            isExpanded: true
        )
        let view = ToolTimelineRowContentView(configuration: config)

        _ = fittedTimelineSize(for: view, width: 320)

        let labels = timelineAllLabels(in: view)
        let titleLabel = try #require(labels.first {
            timelineRenderedText(of: $0).contains("WorkspaceReviewFileDetailView.swift")
        })

        #expect(titleLabel.numberOfLines == 0)
        #expect(titleLabel.lineBreakMode == NSLineBreakMode.byCharWrapping)
    }

    @MainActor
    @Test func expandedReadMediaFallbackTextDoesNotStretchInsideTemporarilyTallCell() throws {
        let path = "/tmp/oppi-screenshots/workspace-home-dense-counts-e2e.png"
        let config = makeTimelineToolConfiguration(
            title: path,
            expandedContent: .readMedia(
                output: "Read image file [image/png]\n[Image: original 1206x2622, displayed at 920x2000.]",
                filePath: path,
                startLine: 1,
                attachments: []
            ),
            toolNamePrefix: "read",
            isExpanded: true
        )
        let view = ToolTimelineRowContentView(configuration: config)
        view.frame = CGRect(x: 0, y: 0, width: 370, height: 620)

        view.setNeedsLayout()
        view.layoutIfNeeded()

        let fallbackLabel = try #require(timelineAllLabels(in: view).first {
            timelineRenderedText(of: $0).contains("Read image file [image/png]")
        })
        let fallbackRect = fallbackLabel.convert(fallbackLabel.bounds, to: view)

        #expect(fallbackRect.minY < 120, "Read-media fallback text should stay near the top; got frame \(fallbackRect)")
        #expect(fallbackRect.height < 96, "Read-media fallback text should not stretch into a blank card; got frame \(fallbackRect)")
    }

    @MainActor
    @Test func trailingByteCountAlignsWithCollapsedTitleRow() throws {
        let config = makeTimelineToolConfiguration(
            title: "$ pwd",
            trailing: "29B",
            isExpanded: false
        )
        let view = ToolTimelineRowContentView(configuration: config)

        _ = fittedTimelineSize(for: view, width: 370)

        let labels = timelineAllLabels(in: view)
        let titleLabel = try #require(labels.first { timelineRenderedText(of: $0) == "$ pwd" })
        let trailingLabel = try #require(labels.first { timelineRenderedText(of: $0) == "29B" })

        let titleRect = titleLabel.convert(titleLabel.bounds, to: view)
        let trailingRect = trailingLabel.convert(trailingLabel.bounds, to: view)

        #expect(abs(trailingRect.minY - titleRect.minY) <= 2)
        #expect(abs(trailingRect.midY - titleRect.midY) <= 3)
    }

    @MainActor
    @Test func dollarPrefixRendersToolIconCenteredWithTitleRow() throws {
        let config = makeTimelineToolConfiguration(
            title: "cd /Users/example/workspace/oppi",
            trailing: nil,
            isExpanded: false
        )
        let view = ToolTimelineRowContentView(configuration: config)

        _ = fittedTimelineSize(for: view, width: 370)

        let labels = timelineAllLabels(in: view)
        let titleLabel = try #require(labels.first {
            timelineRenderedText(of: $0).contains("cd /Users/example/workspace/oppi")
        })
        let titleRect = titleLabel.convert(titleLabel.bounds, to: view)

        let imageViews = timelineAllImageViews(in: view).filter { !$0.isHidden && $0.image != nil }
        let toolIconRect = imageViews
            .map { $0.convert($0.bounds, to: view) }
            .first { rect in
                rect.maxX <= titleRect.minX && rect.width <= 13
            }

        let iconRect = try #require(toolIconRect)
        #expect(abs(iconRect.midY - titleRect.midY) <= 3)
    }

    @MainActor
    @Test func languageBadgeRendersIconInHeaderTrailingArea() throws {
        let config = makeTimelineToolConfiguration(
            title: "read Runtime/TimelineReducer.swift:220-329",
            languageBadge: "Swift",
            toolNamePrefix: "read",
            isExpanded: false
        )
        let view = ToolTimelineRowContentView(configuration: config)

        _ = fittedTimelineSize(for: view, width: 370)

        // Language badge is now an SF Symbol icon (UIImageView), not a text label.
        let imageViews = timelineAllImageViews(in: view)
        let visibleBadge = imageViews.first { !$0.isHidden && $0.image != nil }
        #expect(visibleBadge != nil)
    }

    @MainActor
    @Test func expandedReadMarkdownAddsPinchGestureForFullScreenReader() {
        let config = makeTimelineToolConfiguration(
            expandedContent: .markdown(text: "# Notes\n\n- item"),
            toolNamePrefix: "read",
            isExpanded: true
        )
        let view = ToolTimelineRowContentView(configuration: config)

        _ = fittedTimelineSize(for: view, width: 370)

        let recognizers = timelineAllGestureRecognizers(in: view)
        let hasPinch = recognizers.contains { $0 is UIPinchGestureRecognizer }
        #expect(hasPinch)
    }

    @MainActor
    @Test func expandedReadMarkdownKeepsRowDoubleTapForFullScreen() {
        let config = makeTimelineToolConfiguration(
            expandedContent: .markdown(text: "# Notes\n\n- item"),
            toolNamePrefix: "read",
            isExpanded: true
        )
        let view = ToolTimelineRowContentView(configuration: config)

        _ = fittedTimelineSize(for: view, width: 370)

        #expect(view.expandedTapCopyGestureEnabledForTesting)
    }

    @MainActor
    @Test func expandedReadSwiftKeepsRowDoubleTapForFullScreen() {
        let config = makeTimelineToolConfiguration(
            expandedContent: .code(
                text: "struct Test {}",
                language: .swift,
                startLine: 1,
                filePath: "Test.swift"
            ),
            toolNamePrefix: "read",
            isExpanded: true
        )
        let view = ToolTimelineRowContentView(configuration: config)

        _ = fittedTimelineSize(for: view, width: 370)

        #expect(view.expandedTapCopyGestureEnabledForTesting)
    }

    @MainActor
    @Test func expandedWriteMarkdownKeepsRowDoubleTapForFullScreen() {
        let config = makeTimelineToolConfiguration(
            expandedContent: .markdown(text: "# Notes\n\n- write markdown"),
            toolNamePrefix: "write",
            isExpanded: true
        )
        let view = ToolTimelineRowContentView(configuration: config)

        _ = fittedTimelineSize(for: view, width: 370)

        #expect(view.expandedTapCopyGestureEnabledForTesting)
    }

    @MainActor
    @Test func expandedExtensionMarkdownKeepsRowDoubleTapForFullScreen() {
        let config = makeTimelineToolConfiguration(
            expandedContent: .markdown(text: "extension note"),
            toolNamePrefix: "extensions.notes",
            isExpanded: true
        )
        let view = ToolTimelineRowContentView(configuration: config)

        _ = fittedTimelineSize(for: view, width: 370)

        #expect(view.expandedTapCopyGestureEnabledForTesting)
    }

    @MainActor
    @Test func expandedWriteSwiftKeepsRowDoubleTapForFullScreen() {
        let config = makeTimelineToolConfiguration(
            expandedContent: .code(
                text: "struct Written {}",
                language: .swift,
                startLine: 1,
                filePath: "Written.swift"
            ),
            toolNamePrefix: "write",
            isExpanded: true
        )
        let view = ToolTimelineRowContentView(configuration: config)

        _ = fittedTimelineSize(for: view, width: 370)

        #expect(view.expandedTapCopyGestureEnabledForTesting)
    }

    @MainActor
    @Test func selectedTextCommandEditMenuPrependsCommentActionAndDisablesTapCopy() throws {
        let router = ReviewCommentSelectionRouter { _ in }
        let config = makeTimelineToolConfiguration(
            expandedContent: .bash(command: "echo hi", output: "hi", unwrapped: true),
            copyCommandText: "echo hi",
            copyOutputText: "hi",
            isExpanded: true,
            reviewCommentSelectionRouter: router,
            reviewCommentSessionId: "session-1"
        )
        let view = ToolTimelineRowContentView(configuration: config)

        _ = fittedTimelineSize(for: view, width: 370)

        let commandLabel = try #require(timelineAllTextViews(in: view).first { timelineRenderedText(of: $0) == "echo hi" })
        let menu = try #require(view.textView(
            commandLabel,
            editMenuForTextIn: NSRange(location: 0, length: 4),
            suggestedActions: [UIAction(title: "Copy") { _ in }]
        ))

        let commentAction = try #require(menu.children.first as? UIAction)
        #expect(commentAction.title == "Comment")
        let copyMenuAction = try #require(menu.children.dropFirst().first as? UIAction)
        #expect(copyMenuAction.title == "Copy")
        #expect(commandLabel.isSelectable)
    }

    @MainActor
    @Test func selectedTextExpandedMarkdownKeepsInlineFullScreenActivation() throws {
        let router = ReviewCommentSelectionRouter { _ in }
        let config = makeTimelineToolConfiguration(
            expandedContent: .markdown(text: "Alpha beta gamma"),
            toolNamePrefix: "read",
            isExpanded: true,
            reviewCommentSelectionRouter: router,
            reviewCommentSessionId: "session-1"
        )
        let view = ToolTimelineRowContentView(configuration: config)

        _ = fittedTimelineSize(for: view, width: 370)

        let markdownViewport = try #require(timelineFirstView(ofType: NativeFullScreenMarkdownBody.self, in: view))
        markdownViewport.debugLayoutVisibleMarkdownCellsForTesting()
        let textView = try #require(timelineFirstTextView(in: markdownViewport))
        let menu = markdownViewport.textView(
            textView,
            editMenuForTextIn: NSRange(location: 0, length: 5),
            suggestedActions: [UIAction(title: "Copy") { _ in }]
        )

        #expect(menu == nil)
        #expect(view.expandedTapCopyGestureEnabledForTesting)
    }

    @MainActor
    @Test func commandContextMenuUsesCopyThenCopyOutput() throws {
        let config = makeTimelineToolConfiguration(
            expandedContent: .bash(command: "echo hi", output: "hi", unwrapped: true),
            copyCommandText: "echo hi",
            copyOutputText: "hi",
            isExpanded: true
        )
        let view = ToolTimelineRowContentView(configuration: config)

        let menu = try #require(view.contextMenu(for: .command))
        #expect(timelineActionTitles(in: menu) == ["Copy", "Copy Output"])
    }

    @MainActor

    @Test func outputContextMenuIncludesFullScreenBeforeCopyAndCopyCommand() throws {
        let config = makeTimelineToolConfiguration(
            expandedContent: .bash(command: "echo hi", output: "hi", unwrapped: true),
            copyCommandText: "echo hi",
            copyOutputText: "hi",
            isExpanded: true
        )
        let view = ToolTimelineRowContentView(configuration: config)

        let menu = try #require(view.contextMenu(for: .output))
        #expect(timelineActionTitles(in: menu) == ["Open Full Screen", "Copy", "Copy Command"])
    }

    @MainActor
    @Test func expandedContextMenuPrependsOpenFullScreenBeforeCopy() throws {
        let config = makeTimelineToolConfiguration(
            expandedContent: .markdown(text: "# Notes\n\n- item"),
            copyCommandText: "read docs/notes.md",
            copyOutputText: "# Notes\n\n- item",
            toolNamePrefix: "read",
            isExpanded: true
        )
        let view = ToolTimelineRowContentView(configuration: config)

        let menu = try #require(view.contextMenu(for: .expanded))
        #expect(timelineActionTitles(in: menu) == ["Open Full Screen", "Copy", "Copy Command"])
    }

    @MainActor
    @Test func expandedContextMenuIncludesFullScreenForExtensionMarkdown() throws {
        let config = makeTimelineToolConfiguration(
            expandedContent: .markdown(text: "# Notes\n\nLong-lived extension content"),
            copyCommandText: "extensions.notes save",
            copyOutputText: "# Notes\n\nLong-lived extension content",
            toolNamePrefix: "extensions.notes",
            isExpanded: true
        )
        let view = ToolTimelineRowContentView(configuration: config)

        let menu = try #require(view.contextMenu(for: .expanded))
        #expect(timelineActionTitles(in: menu) == ["Open Full Screen", "Copy", "Copy Command"])
    }

    @MainActor
    @Test func expandedContextMenuIncludesFullScreenForExtensionText() throws {
        let config = makeTimelineToolConfiguration(
            expandedContent: .text(text: "Extension hit #1\nExtension hit #2", language: nil),
            copyCommandText: "extensions.lookup architecture",
            copyOutputText: "Extension hit #1\nExtension hit #2",
            toolNamePrefix: "extensions.lookup",
            isExpanded: true
        )
        let view = ToolTimelineRowContentView(configuration: config)

        let menu = try #require(view.contextMenu(for: .expanded))
        #expect(timelineActionTitles(in: menu) == ["Open Full Screen", "Copy", "Copy Command"])
    }

    @MainActor
    @Test func expandedToolRowsDoNotInstallFloatingFullScreenButton() {
        let shortConfig = makeTimelineToolConfiguration(
            expandedContent: .markdown(text: "# Notes\n\n- item"),
            toolNamePrefix: "read",
            isExpanded: true
        )
        let overflowingConfig = makeTimelineToolConfiguration(
            expandedContent: .markdown(text: "# Notes\n\n" + Array(repeating: "- item", count: 900).joined(separator: "\n")),
            toolNamePrefix: "read",
            isExpanded: true
        )

        let shortView = ToolTimelineRowContentView(configuration: shortConfig)
        _ = fittedTimelineSize(for: shortView, width: 370)

        let overflowingView = ToolTimelineRowContentView(configuration: overflowingConfig)
        _ = fittedTimelineSize(for: overflowingView, width: 370)

        let buttonInShortView = timelineAllViews(in: shortView)
            .compactMap { $0 as? UIButton }
            .first { $0.accessibilityIdentifier == "tool.expand-full-screen" }
        let buttonInOverflowingView = timelineAllViews(in: overflowingView)
            .compactMap { $0 as? UIButton }
            .first { $0.accessibilityIdentifier == "tool.expand-full-screen" }

        #expect(buttonInShortView == nil)
        #expect(buttonInOverflowingView == nil)
    }

    @MainActor
    @Test func emptyExpandedBodyProducesFiniteCompactHeight() {
        let config = makeTimelineToolConfiguration(
            expandedContent: .bash(command: nil, output: nil, unwrapped: true),
            isExpanded: true
        )
        let view = ToolTimelineRowContentView(configuration: config)

        let size = fittedTimelineSize(for: view, width: 370)

        #expect(size.width.isFinite)
        #expect(size.height.isFinite)
        #expect(size.height > 0)
        #expect(size.height < 220)
    }

    @MainActor
    @Test func transitionFromExpandedContentToEmptyBodyStaysFinite() {
        let expanded = makeTimelineToolConfiguration(
            expandedContent: .bash(command: "echo hi", output: "hi", unwrapped: true),
            isExpanded: true
        )
        let emptyExpanded = makeTimelineToolConfiguration(
            expandedContent: .bash(command: nil, output: nil, unwrapped: true),
            isExpanded: true
        )

        let view = ToolTimelineRowContentView(configuration: expanded)
        _ = fittedTimelineSize(for: view, width: 370)

        view.configuration = emptyExpanded
        let size = fittedTimelineSize(for: view, width: 370)

        #expect(size.width.isFinite)
        #expect(size.height.isFinite)
        #expect(size.height > 0)
        #expect(size.height < 220)
    }

    @MainActor
    @Test func reapplyingSameExpandedDiffKeepsRenderedContentStable() throws {
        let config = makeTimelineToolConfiguration(
            expandedContent: .diff(lines: [
                DiffLine(kind: .removed, text: "let value = 1"),
                DiffLine(kind: .added, text: "let value = 2"),
                DiffLine(kind: .context, text: "let unchanged = true"),
            ], path: "src/main.swift"),
            isExpanded: true
        )

        let view = ToolTimelineRowContentView(configuration: config)
        _ = fittedTimelineSize(for: view, width: 370)

        let initialLabel = try #require(timelineAllTextViews(in: view).first {
            timelineRenderedText(of: $0).contains("let value = 2")
        })
        let initialAttributed = try #require(initialLabel.attributedText)

        view.configuration = config
        _ = fittedTimelineSize(for: view, width: 370)

        let updatedLabel = try #require(timelineAllTextViews(in: view).first {
            timelineRenderedText(of: $0).contains("let value = 2")
        })
        let updatedAttributed = try #require(updatedLabel.attributedText)

        #expect(initialAttributed.length == updatedAttributed.length)
        #expect(initialAttributed.string == updatedAttributed.string)
    }

    @MainActor
    @Test func reapplyingSameExpandedExtensionTextReusesRenderedLabelContent() throws {
        let output = """
        EXT-a27df231
        Control tower Live Activity
        in_progress
        """

        let config = makeTimelineToolConfiguration(
            expandedContent: .text(text: output, language: nil),
            toolNamePrefix: "extensions.backlog",
            isExpanded: true
        )

        let view = ToolTimelineRowContentView(configuration: config)
        _ = fittedTimelineSize(for: view, width: 370)

        let initialLabel = try #require(timelineAllTextViews(in: view).first {
            timelineRenderedText(of: $0).contains("EXT-a27df231")
        })
        let initialRendered = timelineRenderedText(of: initialLabel)

        view.configuration = config
        _ = fittedTimelineSize(for: view, width: 370)

        let updatedLabel = try #require(timelineAllTextViews(in: view).first {
            timelineRenderedText(of: $0).contains("EXT-a27df231")
        })
        #expect(timelineRenderedText(of: updatedLabel) == initialRendered)
    }

    @MainActor
    @Test func expandedReadMediaUsesUIKitNativeViewWhenSwiftUIHotPathDisabled() throws {
        let output = "Read image file [image/png]\n\ndata:image/png;base64,\(Self.testPNGBase64)"

        let config = makeTimelineToolConfiguration(
            expandedContent: .readMedia(output: output, filePath: "fixtures/image.png", startLine: 1, attachments: []),
            toolNamePrefix: "read",
            isExpanded: true
        )

        let view = ToolTimelineRowContentView(configuration: config)
        _ = fittedTimelineSize(for: view, width: 370)

        let hosted = timelineAllViews(in: view).first {
            String(describing: type(of: $0)).contains("UIHosting")
        }
        #expect(hosted == nil)

        let hasInlineImageView = timelineAllViews(in: view).contains {
            String(describing: type(of: $0)).contains("NativeExpandedInlineImageView")
        }
        #expect(hasInlineImageView)

        let hasCollapsedPreviewHint = timelineAllTextRenderViews(in: view).contains {
            timelineRenderedText(of: $0).contains("collapsed preview")
        }
        #expect(!hasCollapsedPreviewHint)
    }

    @MainActor
    @Test func expandedReadMediaDoesNotRenderDuplicateImageCards() {
        let output = "Read image file [image/png]\n\ndata:image/png;base64,\(Self.testPNGBase64)"

        let config = makeTimelineToolConfiguration(
            expandedContent: .readMedia(output: output, filePath: "fixtures/image.png", startLine: 1, attachments: []),
            toolNamePrefix: "read",
            isExpanded: true
        )

        let view = ToolTimelineRowContentView(configuration: config)
        _ = fittedTimelineSize(for: view, width: 370)

        let hasExpandedImageCard = timelineAllViews(in: view).contains {
            String(describing: type(of: $0)).contains("TappableImageCard")
        }
        #expect(!hasExpandedImageCard)
    }

    @MainActor
    @Test func expandedReadMediaPromotesRawSVGSourceToInlinePreview() {
        let svg = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 180\"><rect width=\"320\" height=\"180\" fill=\"red\"/></svg>"
        let config = makeTimelineToolConfiguration(
            expandedContent: .readMedia(output: svg, filePath: "fixtures/image.svg", startLine: 1, attachments: []),
            toolNamePrefix: "read",
            isExpanded: true
        )

        let view = ToolTimelineRowContentView(configuration: config)
        _ = fittedTimelineSize(for: view, width: 370)

        let hasInlineImageView = timelineAllViews(in: view).contains {
            $0 is NativeExpandedInlineImageView
        }
        #expect(hasInlineImageView)

        let renderedText = timelineAllLabels(in: view)
            .filter { timelineViewIsVisible($0) }
            .map(timelineRenderedText(of:))
            .joined(separator: "\n")
        #expect(!renderedText.contains("<svg"))
    }

    @MainActor
    @Test func expandedReadMediaSVGShowsSourceToggleDefaultingToPreview() throws {
        let svg = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 180\"><rect width=\"320\" height=\"180\" fill=\"red\"/></svg>"
        let config = makeTimelineToolConfiguration(
            expandedContent: .readMedia(output: svg, filePath: "fixtures/image.svg", startLine: 1, attachments: []),
            toolNamePrefix: "read",
            isExpanded: true
        )

        let view = ToolTimelineRowContentView(configuration: config)
        _ = fittedTimelineSize(for: view, width: 370)

        let control = try #require(timelineAllViews(in: view).compactMap { $0 as? UISegmentedControl }.first)
        #expect(control.selectedSegmentIndex == 0)
        #expect(control.titleForSegment(at: 0) == "Preview")
        #expect(control.titleForSegment(at: 1) == "Source")

        let visibleText = timelineAllLabels(in: view)
            .filter { timelineViewIsVisible($0) }
            .map(timelineRenderedText(of:))
            .joined(separator: "\n")
        #expect(!visibleText.contains("<svg"))

        control.selectedSegmentIndex = 1
        control.sendActions(for: .valueChanged)

        let sourceText = timelineAllLabels(in: view)
            .filter { timelineViewIsVisible($0) }
            .map(timelineRenderedText(of:))
            .joined(separator: "\n")
        #expect(sourceText.contains("<svg"))
    }

    @MainActor
    @Test func expandedReadMediaTruncatedSVGUsesFileFetcherForPreview() async throws {
        let truncatedSVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 180\"><rect width=\"320\" height=\"180\" fill=\"red\"/>"
        let fullSVG = truncatedSVG + "</svg>"
        var config = makeTimelineToolConfiguration(
            expandedContent: .readMedia(output: truncatedSVG, filePath: "fixtures/image.svg", startLine: 1, attachments: []),
            toolNamePrefix: "read",
            isExpanded: true
        )
        config = config.withSessionFileDataFetcher { path in
            #expect(path == "fixtures/image.svg")
            return Data(fullSVG.utf8)
        }

        let view = ToolTimelineRowContentView(configuration: config)
        _ = fittedTimelineSize(for: view, width: 370)

        let hasInlineImageView = timelineAllViews(in: view).contains {
            $0 is NativeExpandedInlineImageView
        }
        #expect(hasInlineImageView)

        let decoded = await waitForTimelineCondition(timeoutMs: 1_400) {
            await MainActor.run {
                timelineAllViews(in: view).contains { candidate in
                    guard candidate is NativeExpandedInlineImageView else { return false }
                    return timelineAllViews(in: candidate).contains { nested in
                        guard let imageView = nested as? UIImageView else { return false }
                        return timelineViewIsVisible(imageView) && imageView.image != nil
                    }
                }
            }
        }
        #expect(decoded)
    }

    @MainActor
    @Test func readMediaVideoFileUsesMediaSourceProviderAndHidesReadToolBoilerplate() throws {
        let videoURL = try #require(URL(string: "https://127.0.0.1:7749/workspaces/ws/raw/clips/demo.mp4"))
        let mediaSource = AuthenticatedMediaSource(
            url: videoURL,
            authorizationHeaderValue: "Bearer test",
            tlsCertFingerprint: nil,
            contentTypeHint: "video/mp4",
            sourceFileExtension: "mp4"
        )
        let view = NativeExpandedReadMediaView()
        view.apply(
            output: "Read video file [video/mp4]",
            isError: false,
            filePath: "clips/demo.mp4",
            startLine: 1,
            attachments: [],
            themeID: ThemeRuntimeState.currentThemeID(),
            audioPlayer: nil,
            sessionId: nil,
            attachmentFetcher: nil,
            sessionFileDataFetcher: nil,
            sessionFileMediaSourceProvider: { path in
                #expect(path == "clips/demo.mp4")
                return mediaSource
            }
        )

        let hasVideoRow = timelineAllViews(in: view).contains { $0 is NativeExpandedVideoAttachmentView }
        #expect(hasVideoRow)

        let visibleText = timelineAllLabels(in: view)
            .filter { timelineViewIsVisible($0) }
            .map(timelineRenderedText(of:))
            .joined(separator: "\n")
        #expect(!visibleText.contains("Read video file [video/mp4]"))
        #expect(visibleText.contains("clips/demo.mp4"))
    }

    @MainActor
    @Test func readMediaVideoFileWithoutStreamURLKeepsFallbackTextAndDoesNotEnableDataFetch() throws {
        let view = NativeExpandedReadMediaView()
        view.apply(
            output: "Read video file [video/mp4]",
            isError: false,
            filePath: "/Users/example/workspace/oppi/clips/demo.mp4",
            startLine: 1,
            attachments: [],
            themeID: ThemeRuntimeState.currentThemeID(),
            audioPlayer: nil,
            sessionId: nil,
            attachmentFetcher: nil,
            sessionFileDataFetcher: { _ in
                Issue.record("Video rows should not download full session file data")
                return Data()
            },
            sessionFileMediaSourceProvider: nil
        )

        let hasVideoRow = timelineAllViews(in: view).contains { $0 is NativeExpandedVideoAttachmentView }
        #expect(!hasVideoRow)

        let visibleText = timelineAllLabels(in: view)
            .filter { timelineViewIsVisible($0) }
            .map(timelineRenderedText(of:))
            .joined(separator: "\n")
        #expect(visibleText.contains("Read video file [video/mp4]"))
    }

    @MainActor
    @Test func expandedReadMediaVideoViewportStaysCompact() throws {
        let videoURL = try #require(URL(string: "https://127.0.0.1:7749/workspaces/ws/raw/clips/demo.mp4"))
        let mediaSource = AuthenticatedMediaSource(
            url: videoURL,
            authorizationHeaderValue: "Bearer test",
            tlsCertFingerprint: nil,
            contentTypeHint: "video/mp4",
            sourceFileExtension: "mp4"
        )
        var configuration = makeTimelineToolConfiguration(
            expandedContent: .readMedia(
                output: "Read video file [video/mp4]",
                filePath: "clips/demo.mp4",
                startLine: 1,
                attachments: []
            ),
            toolNamePrefix: "read",
            isExpanded: true
        )
        configuration = configuration.withSessionFileMediaSourceProvider { path in
            #expect(path == "clips/demo.mp4")
            return mediaSource
        }

        let view = ToolTimelineRowContentView(configuration: configuration)
        _ = fittedTimelineSize(for: view, width: 370)
        let viewport = try #require(
            Mirror(reflecting: view).children.first { $0.label == "expandedViewportHeightConstraint" }?.value as? NSLayoutConstraint
        )

        #expect(
            viewport.constant < 180,
            "Expanded video rows should stay as compact launch rows; inline AVPlayer belongs in full-screen playback. viewport=\(viewport.constant)"
        )
    }

    @MainActor
    @Test func expandedReadMediaViewportGrowsForTallImages() async throws {
        let portraitSVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 200 600\"><rect width=\"200\" height=\"600\" fill=\"red\"/></svg>"
        let landscapeSVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 600 200\"><rect width=\"600\" height=\"200\" fill=\"blue\"/></svg>"

        let portraitView = ToolTimelineRowContentView(configuration: makeTimelineToolConfiguration(
            expandedContent: .readMedia(output: portraitSVG, filePath: "fixtures/portrait.svg", startLine: 1, attachments: []),
            toolNamePrefix: "read",
            isExpanded: true
        ))
        let landscapeView = ToolTimelineRowContentView(configuration: makeTimelineToolConfiguration(
            expandedContent: .readMedia(output: landscapeSVG, filePath: "fixtures/landscape.svg", startLine: 1, attachments: []),
            toolNamePrefix: "read",
            isExpanded: true
        ))

        let portraitContainer = UIView(frame: CGRect(x: 0, y: 0, width: 370, height: 800))
        let landscapeContainer = UIView(frame: CGRect(x: 0, y: 0, width: 370, height: 800))
        for (container, view) in [(portraitContainer, portraitView), (landscapeContainer, landscapeView)] {
            view.translatesAutoresizingMaskIntoConstraints = false
            container.addSubview(view)
            NSLayoutConstraint.activate([
                view.leadingAnchor.constraint(equalTo: container.leadingAnchor),
                view.trailingAnchor.constraint(equalTo: container.trailingAnchor),
                view.topAnchor.constraint(equalTo: container.topAnchor),
            ])
            container.setNeedsLayout()
            container.layoutIfNeeded()
        }

        let portraitViewport = try #require(
            Mirror(reflecting: portraitView).children.first { $0.label == "expandedViewportHeightConstraint" }?.value as? NSLayoutConstraint
        )
        let landscapeViewport = try #require(
            Mirror(reflecting: landscapeView).children.first { $0.label == "expandedViewportHeightConstraint" }?.value as? NSLayoutConstraint
        )

        let updated = await waitForTimelineCondition(timeoutMs: 1_400) {
            await MainActor.run {
                portraitContainer.setNeedsLayout()
                landscapeContainer.setNeedsLayout()
                portraitContainer.layoutIfNeeded()
                landscapeContainer.layoutIfNeeded()
                return portraitViewport.constant > landscapeViewport.constant + 120
            }
        }

        #expect(updated)
        #expect(portraitViewport.constant > 300)
        #expect(landscapeViewport.constant < 260)
    }

    @MainActor
    @Test func inlineSVGPreviewAppliesAspectRatioConstraintAfterAsyncDecode() async throws {
        let svg = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 180\"><rect width=\"320\" height=\"180\" fill=\"red\"/></svg>"
        let container = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 400))
        let view = NativeExpandedInlineImageView(maxPixelSize: 1_600)
        view.translatesAutoresizingMaskIntoConstraints = false
        container.addSubview(view)
        NSLayoutConstraint.activate([
            view.leadingAnchor.constraint(equalTo: container.leadingAnchor),
            view.trailingAnchor.constraint(equalTo: container.trailingAnchor),
            view.topAnchor.constraint(equalTo: container.topAnchor),
        ])
        view.apply(base64: Data(svg.utf8).base64EncodedString(), mimeType: "image/svg+xml")

        let updated = await waitForTimelineCondition(timeoutMs: 1_400) {
            await MainActor.run {
                container.setNeedsLayout()
                container.layoutIfNeeded()
                let fitted = view.systemLayoutSizeFitting(
                    CGSize(width: 320, height: UIView.layoutFittingCompressedSize.height),
                    withHorizontalFittingPriority: .required,
                    verticalFittingPriority: .fittingSizeLevel
                )
                return abs(fitted.height - 180) < 2
            }
        }

        #expect(updated, "Expected SVG preview height to track the viewBox aspect ratio")
    }

    @MainActor
    @Test func expandedReadMediaViewportUsesSVGWidthHeightWhenViewBoxMissing() async throws {
        let svg = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"320\" height=\"180\"><rect width=\"320\" height=\"180\" fill=\"red\"/></svg>"
        let view = ToolTimelineRowContentView(configuration: makeTimelineToolConfiguration(
            expandedContent: .readMedia(output: svg, filePath: "fixtures/chart.svg", startLine: 1, attachments: []),
            toolNamePrefix: "read",
            isExpanded: true
        ))
        let container = UIView(frame: CGRect(x: 0, y: 0, width: 370, height: 700))
        view.translatesAutoresizingMaskIntoConstraints = false
        container.addSubview(view)
        NSLayoutConstraint.activate([
            view.leadingAnchor.constraint(equalTo: container.leadingAnchor),
            view.trailingAnchor.constraint(equalTo: container.trailingAnchor),
            view.topAnchor.constraint(equalTo: container.topAnchor),
        ])

        let updated = await waitForTimelineCondition(timeoutMs: 1_400) {
            await MainActor.run {
                container.setNeedsLayout()
                container.layoutIfNeeded()
                let renderedHeight = view.systemLayoutSizeFitting(
                    CGSize(width: 370, height: UIView.layoutFittingCompressedSize.height),
                    withHorizontalFittingPriority: .required,
                    verticalFittingPriority: .fittingSizeLevel
                ).height
                return renderedHeight > 250
            }
        }

        #expect(updated, "Expected expanded SVG read preview to grow beyond the collapsed fallback height when only width/height are present")
    }

    @MainActor
    @Test func inlineSVGPreviewAppliesAspectRatioConstraintFromWidthHeightAttributes() async throws {
        let svg = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"320\" height=\"180\"><rect width=\"320\" height=\"180\" fill=\"red\"/></svg>"
        let container = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 400))
        let view = NativeExpandedInlineImageView(maxPixelSize: 1_600)
        view.translatesAutoresizingMaskIntoConstraints = false
        container.addSubview(view)
        NSLayoutConstraint.activate([
            view.leadingAnchor.constraint(equalTo: container.leadingAnchor),
            view.trailingAnchor.constraint(equalTo: container.trailingAnchor),
            view.topAnchor.constraint(equalTo: container.topAnchor),
        ])
        view.apply(base64: Data(svg.utf8).base64EncodedString(), mimeType: "image/svg+xml")

        let updated = await waitForTimelineCondition(timeoutMs: 1_400) {
            await MainActor.run {
                container.setNeedsLayout()
                container.layoutIfNeeded()
                let fitted = view.systemLayoutSizeFitting(
                    CGSize(width: 320, height: UIView.layoutFittingCompressedSize.height),
                    withHorizontalFittingPriority: .required,
                    verticalFittingPriority: .fittingSizeLevel
                )
                return abs(fitted.height - 180) < 2
            }
        }

        #expect(updated, "Expected SVG preview height to track width/height attributes when viewBox is missing")
    }

    @MainActor
    @Test func repeatedExpandedExtensionTextReconfigureStaysWithinBudget() {
        let output = """
        EXT-a27df231
        Control tower Live Activity
        in_progress
        """

        let config = makeTimelineToolConfiguration(
            expandedContent: .text(text: output, language: nil),
            toolNamePrefix: "extensions.backlog",
            isExpanded: true
        )

        let view = ToolTimelineRowContentView(configuration: config)
        _ = fittedTimelineSize(for: view, width: 370)

        let start = ContinuousClock.now
        for _ in 0..<120 {
            view.configuration = config
        }
        let elapsed = ContinuousClock.now - start

        #expect(elapsed < .milliseconds(600), "120 identical extension reconfigures took \(elapsed)")
    }

    @MainActor
    @Test func expandedOutputUsesCappedViewportHeight() {
        let longOutput = Array(repeating: "line", count: 600).joined(separator: "\n")
        let config = makeTimelineToolConfiguration(
            expandedContent: .bash(command: "echo hi", output: longOutput, unwrapped: true),
            isExpanded: true
        )

        let view = ToolTimelineRowContentView(configuration: config)
        let size = fittedTimelineSize(for: view, width: 370)

        #expect(size.width.isFinite)
        #expect(size.height.isFinite)
        #expect(size.height > 300)
        #expect(size.height < 760)
    }

    @MainActor
    @Test func expandedOutputCanUseUnwrappedTerminalLayout() throws {
        let config = makeTimelineToolConfiguration(
            expandedContent: .bash(
                command: "tail -16 build.log",
                output: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
                unwrapped: true
            ),
            isExpanded: true
        )

        let view = ToolTimelineRowContentView(configuration: config)
        _ = fittedTimelineSize(for: view, width: 280)

        let outputLabel = try #require(timelineAllTextViews(in: view).first {
            timelineRenderedText(of: $0).contains("0123456789abcdefghijklmnopqrstuvwxyz")
        })
        #expect(outputLabel.textContainer.lineBreakMode == .byClipping)

        let horizontalScroll = timelineAllScrollViews(in: view).first { $0.showsHorizontalScrollIndicator }
        #expect(horizontalScroll != nil)
    }

    @MainActor
    @Test func expandedMarkdownReadUsesNativeMarkdownViewWithCodeBlockSubview() {
        let markdown = """
        # Header

        Wrapped prose paragraph that should render as markdown text.

        ```swift
        let reallyLongLine = \"this code line should not soft wrap inside markdown code block rendering\"
        ```
        """

        let config = makeTimelineToolConfiguration(
            expandedContent: .markdown(text: markdown),
            isExpanded: true
        )

        let view = ToolTimelineRowContentView(configuration: config)
        _ = fittedTimelineSize(for: view, width: 300)

        let markdownView = timelineFirstView(ofType: AssistantMarkdownContentView.self, in: view)
        #expect(markdownView != nil)

        let codeBlockView = timelineFirstView(ofType: NativeCodeBlockView.self, in: view)
        #expect(codeBlockView != nil)
    }

    @MainActor
    @Test func expandedMarkdownInitialSizingBeforeLayoutPassAvoidsViewportMaxJump() {
        let config = makeTimelineToolConfiguration(
            expandedContent: .markdown(text: "Oppi repo normalized per request."),
            toolNamePrefix: "extensions.notes",
            isExpanded: true
        )

        let view = ToolTimelineRowContentView(configuration: config)
        let firstPassSize = fittedTimelineSizeWithoutPrelayout(for: view, width: 300)

        #expect(firstPassSize.height.isFinite)
        #expect(firstPassSize.height > 0)
        #expect(
            firstPassSize.height < 260,
            "Initial markdown sizing should stay compact; got \(firstPassSize.height)"
        )
    }

    @MainActor
    @Test func expandedPlainTextInitialSizingBeforeLayoutPassAvoidsViewportMaxJump() {
        let config = makeTimelineToolConfiguration(
            expandedContent: .text(
                text: "extension text: Oppi iOS bash tool-row header updated, tags: [6 items]",
                language: nil
            ),
            toolNamePrefix: "extensions.notes",
            isExpanded: true
        )

        let view = ToolTimelineRowContentView(configuration: config)
        let firstPassSize = fittedTimelineSizeWithoutPrelayout(for: view, width: 300)

        #expect(firstPassSize.height.isFinite)
        #expect(firstPassSize.height > 0)
        #expect(
            firstPassSize.height < 260,
            "Initial wrapped text sizing should stay compact; got \(firstPassSize.height)"
        )
    }

    @MainActor
    @Test func expandedMarkdownReadPreservesTailContentPastGenericTruncationLimit() {
        let longParagraph = String(repeating: "markdown-content-", count: 160)
        let markdown = """
        # Header

        \(longParagraph)

        ## Tail Marker
        Tail content should remain visible in markdown mode.
        """

        let config = makeTimelineToolConfiguration(
            expandedContent: .markdown(text: markdown),
            isExpanded: true
        )

        let view = ToolTimelineRowContentView(configuration: config)
        _ = fittedTimelineSize(for: view, width: 300)

        let markdownViewport = timelineFirstView(ofType: NativeFullScreenMarkdownBody.self, in: view)
        let rendered = timelineAllTextViews(in: view)
            .map { $0.attributedText?.string ?? $0.text ?? "" }
            .joined(separator: "\n")

        #expect(markdownViewport?.debugSourceTextForTesting.contains("Tail Marker") == true)
        #expect(!rendered.contains("output truncated for display"))
    }

    @MainActor
    @Test func expandedOutputDisplayKeepsLargePayloadsIntact() throws {
        let longOutput = String(repeating: "x", count: 12_000)
        let config = makeTimelineToolConfiguration(
            expandedContent: .bash(command: nil, output: longOutput, unwrapped: true),
            isExpanded: true
        )

        let view = ToolTimelineRowContentView(configuration: config)
        _ = fittedTimelineSize(for: view, width: 370)

        let renderedTexts = timelineAllTextRenderViews(in: view).map { timelineRenderedText(of: $0) }
        let longest = try #require(renderedTexts.max(by: { $0.count < $1.count }))

        #expect(longest.contains(longOutput))
        #expect(!longest.contains("output truncated for display"))
    }

    @MainActor
    @Test func expandedDiffIncreasesBodyHeight() {
        let collapsed = makeTimelineToolConfiguration(isExpanded: false)
        let expanded = makeTimelineToolConfiguration(
            expandedContent: .diff(lines: [
                DiffLine(kind: .removed, text: "let value = 1"),
                DiffLine(kind: .added, text: "let value = 2"),
                DiffLine(kind: .context, text: "let unchanged = true"),
            ], path: "src/main.swift"),
            isExpanded: true
        )

        let collapsedView = ToolTimelineRowContentView(configuration: collapsed)
        let expandedView = ToolTimelineRowContentView(configuration: expanded)

        let collapsedSize = fittedTimelineSize(for: collapsedView, width: 370)
        let expandedSize = fittedTimelineSize(for: expandedView, width: 370)

        #expect(expandedSize.height > collapsedSize.height)
    }

    @MainActor
    @Test func expandedDiffShowsGutterBarsAndPrefixes() {
        let config = makeTimelineToolConfiguration(
            expandedContent: .diff(lines: [
                DiffLine(kind: .removed, text: "let value = 1"),
                DiffLine(kind: .added, text: "let value = 2"),
            ], path: "src/main.swift"),
            isExpanded: true
        )

        let view = ToolTimelineRowContentView(configuration: config)
        _ = fittedTimelineSize(for: view, width: 370)

        // Diff text is rendered into the expanded UITextView inside the viewport.
        let rendered = timelineAllTextViews(in: view)
            .compactMap { $0.attributedText?.string ?? $0.text }
            .joined(separator: "\n")

        // Gutter with prefix ( + /  - ) should be present.
        #expect(rendered.contains(" + "))
        #expect(rendered.contains(" - "))
        #expect(rendered.contains("let value"))
        #expect(!rendered.contains("Change 1 of 1"))
        #expect(!rendered.contains("@@ -"), "Expanded diff view should hide raw unified-diff hunk headers")
    }

    @MainActor
    @Test func expandedEmptyDiffShowsNoTextualChangesMessage() {
        let config = makeTimelineToolConfiguration(
            expandedContent: .diff(lines: [], path: "src/main.swift"),
            isExpanded: true
        )

        let view = ToolTimelineRowContentView(configuration: config)
        _ = fittedTimelineSize(for: view, width: 370)

        let rendered = timelineAllTextRenderViews(in: view)
            .map { timelineRenderedText(of: $0) }
            .joined(separator: "\n")

        #expect(rendered.contains("No textual changes"))
    }

    @MainActor
    @Test func errorOutputPresentationStripsANSIEscapeCodes() {
        let input = "\u{001B}[31mFAIL\u{001B}[39m tests/workspace-crud.test.ts"

        let presentation = ToolRowTextRenderer.makeANSIOutputPresentation(
            input,
            isError: true
        )

        let rendered = presentation.attributedText?.string ?? presentation.plainText ?? ""
        #expect(rendered == "FAIL tests/workspace-crud.test.ts")
        #expect(!rendered.contains("[31m"))
        #expect(!rendered.contains("[39m"))
    }

    @MainActor
    @Test func errorOutputFallbackStillStripsANSIWhenHighlightingSkipped() {
        let input = "\u{001B}[31mFAIL\u{001B}[39m " + String(repeating: "x", count: 80)

        let presentation = ToolRowTextRenderer.makeANSIOutputPresentation(
            input,
            isError: true,
            maxHighlightBytes: 8
        )

        #expect(presentation.attributedText == nil)
        let rendered = presentation.plainText ?? ""
        #expect(rendered.hasPrefix("FAIL "))
        #expect(!rendered.contains("[31m"))
        #expect(!rendered.contains("[39m"))
    }

    @MainActor
    @Test func syntaxOutputPresentationHighlightsKnownLanguage() {
        let source = "guard value else { return }"

        let presentation = ToolRowTextRenderer.makeSyntaxOutputPresentation(
            source,
            language: .swift
        )

        #expect(presentation.plainText == nil)
        #expect(presentation.attributedText?.string == source)
    }

    @MainActor
    @Test func ansiHighlightedSeparatedOutputRemainsVisible() {
        let config = makeTimelineToolConfiguration(
            expandedContent: .bash(
                command: "echo hi",
                output: "\u{001B}[31mFAIL\u{001B}[39m tests/workspace-crud.test.ts",
                unwrapped: true
            ),
            isExpanded: true,
            isError: true
        )
        let view = ToolTimelineRowContentView(configuration: config)

        let renderedTexts = timelineAllTextRenderViews(in: view)
            .map { timelineRenderedText(of: $0) }

        #expect(renderedTexts.contains { $0.contains("FAIL tests/workspace-crud.test.ts") })
    }

    // MARK: - Collapsed Image Row Presentation

    // Minimal valid 1x1 red-pixel PNG for testing (82 bytes, base64).
    private static let testPNGBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg=="

    @MainActor
    @Test func collapsedReadImageMatchesPlainReadHeight() {
        let plain = makeTimelineToolConfiguration(
            title: "server.ts",
            toolNamePrefix: "read",
            isExpanded: false
        )
        let imageLikeRead = makeTimelineToolConfiguration(
            title: "icon.png",
            languageBadge: FileType.image.displayLabel,
            toolNamePrefix: "read",
            isExpanded: false
        )

        let plainView = ToolTimelineRowContentView(configuration: plain)
        let imageView = ToolTimelineRowContentView(configuration: imageLikeRead)

        let plainSize = fittedTimelineSize(for: plainView, width: 370)
        let imageSize = fittedTimelineSize(for: imageView, width: 370)

        #expect(abs(imageSize.height - plainSize.height) < 24)
    }

    @MainActor
    @Test func collapsedReadImageDoesNotRenderInlinePreview() {
        let config = makeTimelineToolConfiguration(
            title: "icon.png",
            languageBadge: FileType.image.displayLabel,
            toolNamePrefix: "read",
            isExpanded: false
        )
        let view = ToolTimelineRowContentView(configuration: config)
        _ = fittedTimelineSize(for: view, width: 370)

        let previewImageViews = timelineAllImageViews(in: view)
            .filter { !$0.isHidden && $0.image != nil && $0.contentMode == .scaleAspectFit }
            .filter { $0.superview?.layer.cornerRadius == 6 }
            .filter { !($0.superview?.isHidden ?? true) }

        #expect(previewImageViews.isEmpty)
    }

    @MainActor
    @Test func imageBadgeUsesPhotoIcon() {
        #expect(ToolTimelineRowUIHelpers.languageBadgeImage(for: FileType.image.displayLabel) != nil)
    }

    @MainActor
    @Test func expandedVoiceMessagePlayButtonIsHitTestTarget() throws {
        let source = AuthenticatedMediaSource(
            url: try #require(URL(string: "https://127.0.0.1:7749/sessions/s1/attachments/att-session-owned-voice")),
            authorizationHeaderValue: "Bearer test",
            tlsCertFingerprint: nil,
            contentTypeHint: "audio/wav",
            sourceFileExtension: "wav"
        )
        let config = makeTimelineToolConfiguration(
            title: "Voice message",
            expandedContent: .audioMessage(
                text: "Replay this exact session-owned attachment.",
                attachmentId: "att-session-owned-voice",
                mimeType: "audio/wav",
                durationSeconds: 1.0,
                playbackBehavior: .playNow
            ),
            toolNamePrefix: "voice_speak",
            toolNameColor: .systemPurple,
            isExpanded: true
        )
            .withAudioPlayer(AudioPlayerService())
            .withSessionAttachmentMediaSourceProvider { _, _, _ in source }
        let view = ToolTimelineRowContentView(configuration: config)
        view.frame = CGRect(origin: .zero, size: fittedTimelineSize(for: view, width: 370))
        view.layoutIfNeeded()

        let button = try #require(timelineAllViews(in: view).compactMap { $0 as? UIButton }.first { !$0.isHidden })
        let point = button.convert(CGPoint(x: button.bounds.midX, y: button.bounds.midY), to: view)

        #expect(view.hitTest(point, with: nil) === button)
    }

    @MainActor
    @Test func collapsedVoiceMessagePlayButtonIsHitTestTarget() throws {
        let source = AuthenticatedMediaSource(
            url: try #require(URL(string: "https://127.0.0.1:7749/sessions/s1/attachments/att-session-owned-voice")),
            authorizationHeaderValue: "Bearer test",
            tlsCertFingerprint: nil,
            contentTypeHint: "audio/wav",
            sourceFileExtension: "wav"
        )
        let config = makeTimelineToolConfiguration(
            title: "Voice message",
            expandedContent: .audioMessage(
                text: "Replay this exact session-owned attachment.",
                attachmentId: "att-session-owned-voice",
                mimeType: "audio/wav",
                durationSeconds: 1.0,
                playbackBehavior: .tapToPlay
            ),
            toolNamePrefix: "voice_speak",
            toolNameColor: .systemPurple,
            isExpanded: false
        )
            .withAudioPlayer(AudioPlayerService())
            .withSessionAttachmentMediaSourceProvider { _, _, _ in source }
        let view = ToolTimelineRowContentView(configuration: config)
        view.frame = CGRect(origin: .zero, size: fittedTimelineSize(for: view, width: 370))
        view.layoutIfNeeded()

        let button = try #require(timelineAllViews(in: view).compactMap { $0 as? UIButton }.first { !$0.isHidden })
        let point = button.convert(CGPoint(x: button.bounds.midX, y: button.bounds.midY), to: view)

        #expect(view.hitTest(point, with: nil) === button)
    }

    @MainActor
    @Test func expandedVoiceMessageOnlyShowsHeaderPlaybackControl() throws {
        let source = AuthenticatedMediaSource(
            url: try #require(URL(string: "https://127.0.0.1:7749/sessions/s1/attachments/att-session-owned-voice")),
            authorizationHeaderValue: "Bearer test",
            tlsCertFingerprint: nil,
            contentTypeHint: "audio/wav",
            sourceFileExtension: "wav"
        )
        let config = makeTimelineToolConfiguration(
            title: "Voice message",
            expandedContent: .audioMessage(
                text: "Got it. I will treat both collapsed and expanded playback as broken, not just the expanded card.",
                attachmentId: "att-session-owned-voice",
                mimeType: "audio/wav",
                durationSeconds: 1.0,
                playbackBehavior: .tapToPlay
            ),
            toolNamePrefix: "voice_speak",
            toolNameColor: .systemPurple,
            isExpanded: true
        )
            .withAudioPlayer(AudioPlayerService())
            .withSessionAttachmentMediaSourceProvider { _, _, _ in source }
        let view = ToolTimelineRowContentView(configuration: config)
        view.frame = CGRect(origin: .zero, size: fittedTimelineSize(for: view, width: 370))
        view.layoutIfNeeded()

        let visibleButtons = timelineAllViews(in: view).compactMap { $0 as? UIButton }.filter { !$0.isHidden }
        #expect(visibleButtons.count == 1)

        let voiceMessageLabels = timelineAllLabels(in: view).filter {
            timelineRenderedText(of: $0).trimmingCharacters(in: .whitespacesAndNewlines) == "Voice message"
        }
        #expect(voiceMessageLabels.count == 1)
    }

    @MainActor
    @Test func collapsedVoiceMessagePlayButtonRequestsMediaSourceOnTap() async throws {
        actor SourceSpy {
            private(set) var count = 0
            let mediaSource: AuthenticatedMediaSource

            init(mediaSource: AuthenticatedMediaSource) {
                self.mediaSource = mediaSource
            }

            func source(_ attachmentId: String) -> AuthenticatedMediaSource {
                count += 1
                #expect(attachmentId == "att-session-owned-voice")
                return mediaSource
            }

            func value() -> Int { count }
        }

        let mediaSource = AuthenticatedMediaSource(
            url: try #require(URL(string: "https://127.0.0.1:7749/sessions/s1/attachments/att-session-owned-voice")),
            authorizationHeaderValue: "Bearer test",
            tlsCertFingerprint: nil,
            contentTypeHint: "audio/wav",
            sourceFileExtension: "wav"
        )
        let sourceSpy = SourceSpy(mediaSource: mediaSource)
        let config = makeTimelineToolConfiguration(
            title: "Voice message",
            expandedContent: .audioMessage(
                text: "Replay this exact session-owned attachment.",
                attachmentId: "att-session-owned-voice",
                mimeType: "audio/wav",
                durationSeconds: 1.0,
                playbackBehavior: .playNow
            ),
            toolNamePrefix: "voice_speak",
            toolNameColor: .systemPurple,
            isExpanded: false
        )
            .withAudioPlayer(AudioPlayerService())
            .withSessionAttachmentMediaSourceProvider { attachmentId, _, _ in
                await sourceSpy.source(attachmentId)
            }
        let view = ToolTimelineRowContentView(configuration: config)

        _ = fittedTimelineSize(for: view, width: 370)

        let button = try #require(timelineAllViews(in: view).compactMap { $0 as? UIButton }.first { !$0.isHidden })
        button.sendActions(for: .touchUpInside)

        let didRequestSource = await waitForTestCondition(timeoutMs: 300, pollMs: 10) {
            await sourceSpy.value() > 0
        }
        #expect(didRequestSource)
        #expect(await sourceSpy.value() == 1)
    }

}
