import Foundation
import Testing

@testable import NucleicCore

@Suite struct AttachmentMaterializerTests {
    /// Make a throwaway worktree directory and clean it up after the body runs.
    private func withTempWorktree(_ body: (String) throws -> Void) throws {
        let dir = (NSTemporaryDirectory() as NSString)
            .appendingPathComponent("nucleic-attach-\(UUID().uuidString)")
        try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
        defer { try? FileManager.default.removeItem(atPath: dir) }
        try body(dir)
    }

    @Test func materializeWritesFilesAndReturnsRelativePaths() throws {
        try withTempWorktree { worktree in
            let attachments = [
                PendingAttachment(filename: "screenshot.png", data: Data([0x1, 0x2, 0x3])),
                PendingAttachment(filename: "notes.txt", data: Data("hello".utf8)),
            ]
            let paths = try AttachmentMaterializer.materialize(attachments, intoWorktree: worktree)

            #expect(paths.count == 2)
            // Each lands under the git-excluded attachments dir, keeping its (sanitized) name.
            #expect(paths[0].hasPrefix("\(AttachmentMaterializer.directory)/"))
            #expect(paths[0].hasSuffix("-screenshot.png"))
            #expect(paths[1].hasSuffix("-notes.txt"))

            // The bytes actually hit disk at the returned path, relative to the worktree root.
            for (path, expected) in zip(paths, attachments) {
                let url = URL(fileURLWithPath: worktree).appendingPathComponent(path)
                #expect(try Data(contentsOf: url) == expected.data)
            }
        }
    }

    @Test func materializeKeepsSameNamedFilesDistinct() throws {
        try withTempWorktree { worktree in
            let attachments = [
                PendingAttachment(filename: "image.png", data: Data([0xA])),
                PendingAttachment(filename: "image.png", data: Data([0xB])),
            ]
            let paths = try AttachmentMaterializer.materialize(attachments, intoWorktree: worktree)
            #expect(paths.count == 2)
            #expect(paths[0] != paths[1])  // per-file token prevents a collision
        }
    }

    @Test func emptyAttachmentsAreANoOp() throws {
        try withTempWorktree { worktree in
            let paths = try AttachmentMaterializer.materialize([], intoWorktree: worktree)
            #expect(paths.isEmpty)
            // No directory is created when there's nothing to write.
            let dir = URL(fileURLWithPath: worktree)
                .appendingPathComponent(AttachmentMaterializer.directory)
            #expect(!FileManager.default.fileExists(atPath: dir.path))
        }
    }

    @Test func sanitizeStripsPathsAndUnsafeCharacters() {
        // Path components are dropped — a name can never escape the attachments dir.
        #expect(AttachmentMaterializer.sanitizeFilename("../../etc/passwd") == "passwd")
        #expect(AttachmentMaterializer.sanitizeFilename("a/b/c.png") == "c.png")
        // Odd characters become underscores; safe ones are kept.
        #expect(AttachmentMaterializer.sanitizeFilename("my file (1).png") == "my_file_(1).png")
        // Degenerate names fall back to a usable default.
        #expect(AttachmentMaterializer.sanitizeFilename("") == "attachment")
        #expect(AttachmentMaterializer.sanitizeFilename("..") == "attachment")
    }

    @Test func augmentAppendsReferenceBlock() {
        let withText = AttachmentMaterializer.augment(
            message: "take a look", withReferencePaths: [".nucleic/attachments/a.png"])
        #expect(withText.hasPrefix("take a look\n\n"))
        #expect(withText.contains("- .nucleic/attachments/a.png"))
        #expect(withText.contains("Attached file"))

        // An attachment-only message stands on its own with a leading instruction.
        let noText = AttachmentMaterializer.augment(
            message: "", withReferencePaths: [".nucleic/attachments/a.png", ".nucleic/attachments/b.png"])
        #expect(noText.contains("Please take a look at the attached files"))
        #expect(noText.contains("- .nucleic/attachments/a.png"))
        #expect(noText.contains("- .nucleic/attachments/b.png"))

        // No paths ⇒ message is returned unchanged.
        #expect(AttachmentMaterializer.augment(message: "hi", withReferencePaths: []) == "hi")
    }
}
