import Foundation
import NucleicCore
import Testing

@testable import NucleicApp

@Suite("Markdown render disk cache", .serialized)
struct MarkdownRenderDiskCacheTests {
    @Test func coldParseRoundTripsAsAttributedRuns() async throws {
        let fixture = makeFixture()
        defer { try? FileManager.default.removeItem(at: fixture.root) }
        let markdown = "A **bold** value, some `code`, and a [link](https://example.com)."

        MarkdownText.clearMemoryCachesForTesting()
        let cold = await MarkdownText.prewarm(
            [markdown], sessionID: fixture.session, diskCache: fixture.cache)
        #expect(cold.parsed > 0)
        #expect(cold.diskHits == 0)

        MarkdownText.clearMemoryCachesForTesting()
        let reopened = await MarkdownText.prewarm(
            [markdown], sessionID: fixture.session, diskCache: fixture.cache)
        #expect(reopened.parsed == 0)
        #expect(reopened.diskHits == cold.parsed)

        let rendered = try #require(MarkdownText.cachedInlineForTesting(markdown))
        #expect(String(rendered.characters) == "A bold value, some code, and a link.")
        #expect(rendered.runs.contains {
            $0.inlinePresentationIntent?.contains(.stronglyEmphasized) == true
        })
        #expect(rendered.runs.contains {
            $0.inlinePresentationIntent?.contains(.code) == true
        })
        #expect(rendered.runs.contains { $0.link?.absoluteString == "https://example.com" })
    }

    @Test func changedMarkdownIsAColdMissAndReplacesTheSessionSnapshot() async {
        let fixture = makeFixture()
        defer { try? FileManager.default.removeItem(at: fixture.root) }

        MarkdownText.clearMemoryCachesForTesting()
        _ = await MarkdownText.prewarm(
            ["**first**"], sessionID: fixture.session, diskCache: fixture.cache)
        MarkdownText.clearMemoryCachesForTesting()
        let changed = await MarkdownText.prewarm(
            ["**second**"], sessionID: fixture.session, diskCache: fixture.cache)

        #expect(changed.diskHits == 0)
        #expect(changed.parsed == 1)
    }

    @Test func tailFirstMemoryWarmDoesNotReplaceTheSessionSnapshot() async {
        let fixture = makeFixture()
        defer { try? FileManager.default.removeItem(at: fixture.root) }
        let older = "An **older** response."
        let tail = "The `visible` tail."

        MarkdownText.clearMemoryCachesForTesting()
        _ = await MarkdownText.prewarm(
            [older], sessionID: fixture.session, diskCache: fixture.cache)
        MarkdownText.clearMemoryCachesForTesting()
        let firstPaint = await MarkdownText.prewarm(
            [tail], sessionID: fixture.session, diskCache: fixture.cache,
            diskAccess: .memoryOnly)
        #expect(firstPaint.diskHits == 0)
        #expect(firstPaint.parsed == 1)

        // Model a relaunch: the older snapshot remains, while the tail was intentionally not
        // persisted by the first-paint pass and is a cold miss for the complete follow-up.
        MarkdownText.clearMemoryCachesForTesting()
        let reopenedCache = MarkdownRenderDiskCache(directory: fixture.root)
        let complete = await MarkdownText.prewarm(
            [older, tail], sessionID: fixture.session, diskCache: reopenedCache)

        #expect(complete.parsed == 1)
        #expect(complete.diskHits == 1)
    }

    @Test func corruptCacheFallsBackToParsing() async throws {
        let fixture = makeFixture()
        defer { try? FileManager.default.removeItem(at: fixture.root) }
        let markdown = "A **recoverable** message."

        MarkdownText.clearMemoryCachesForTesting()
        _ = await MarkdownText.prewarm(
            [markdown], sessionID: fixture.session, diskCache: fixture.cache)
        try Data("not a property list".utf8).write(
            to: fixture.cache.fileURL(for: fixture.session), options: .atomic)

        MarkdownText.clearMemoryCachesForTesting()
        // Model a new process: a live cache actor intentionally retains its eight most recently
        // loaded sessions and would not reread a file that changed behind its back.
        let reopenedCache = MarkdownRenderDiskCache(directory: fixture.root)
        let recovered = await MarkdownText.prewarm(
            [markdown], sessionID: fixture.session, diskCache: reopenedCache)
        #expect(recovered.diskHits == 0)
        #expect(recovered.parsed > 0)
    }

    private func makeFixture() -> (
        root: URL, cache: MarkdownRenderDiskCache, session: SessionID
    ) {
        let root = FileManager.default.temporaryDirectory
            .appendingPathComponent("markdown-cache-tests-\(UUID().uuidString)", isDirectory: true)
        return (
            root,
            MarkdownRenderDiskCache(directory: root),
            SessionID(rawValue: "markdown-cache-test"))
    }
}
