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

import ContainerTestSupport
import Darwin
import Foundation
import Testing

// Convenience alias for the verbose entry type.
typealias FSEntry = ContainerFixture.FileSystemEntry

struct TestCLIBuilder {

    // MARK: - Basic build tests

    @Test func testBuildDefaultParams() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(dir: dir, dockerfile: "FROM ghcr.io/linuxcontainers/alpine:3.20")
            // No tags — runtime generates one and prints it to stdout.
            let output = try f.buildWithPaths(contextDir: dir)
            let generatedTag = output.trimmingCharacters(in: .whitespacesAndNewlines)
            #expect(!generatedTag.isEmpty, "build should print the generated image tag to stdout")
            try f.assertImageBuilt(generatedTag)
        }
    }

    @Test func testBuildDotFileSucceeds() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: "FROM scratch\nADD emptyFile /",
                context: [
                    .file("emptyFile", content: .zeroFilled(size: 1)),
                    .file(".dockerignore", content: .data(".dockerignore\n".data(using: .utf8)!)),
                ])
            let image = "registry.local/dot-file:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir)
            try f.assertImageBuilt(image)
        }
    }

    @Test func testBuildFromPreviousStage() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: """
                    FROM ghcr.io/linuxcontainers/alpine:3.20 AS layer1
                    RUN sh -c "echo 'layer1' > /layer1.txt"
                    FROM layer1
                    CMD ["cat", "/layer1.txt"]
                    """)
            let image = "registry.local/from-previous-layer:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir)
            try f.assertImageBuilt(image)
        }
    }

    @Test func testBuildFromLocalImage() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: "FROM scratch\nADD emptyFile /",
                context: [
                    .file("emptyFile", content: .zeroFilled(size: 0)),
                    .file(".dockerignore", content: .data(".dockerignore\n".data(using: .utf8)!)),
                ])
            let image = "local-only:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir)
            try f.assertImageBuilt(image)

            let dir2 = try f.createTempDir()
            try f.createContext(
                dir: dir2,
                dockerfile: "FROM \(image)",
                context: [])
            let image2 = "from-local:\(UUID().uuidString)"
            try f.build(tag: image2, contextDir: dir2)
            try f.assertImageBuilt(image2)
        }
    }

    @Test func testBuildAddFromSpecialDirs() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: "FROM scratch\nADD emptyFile /",
                context: [.file("emptyFile", content: .zeroFilled(size: 1))])
            let image = "registry.local/scratch-add-special-dir:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir)
            try f.assertImageBuilt(image)
        }
    }

    @Test func testBuildScratchAdd() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: "FROM scratch\nADD emptyFile /",
                context: [.file("emptyFile", content: .zeroFilled(size: 1))])
            let image = "registry.local/scratch-add:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir)
            try f.assertImageBuilt(image)
        }
    }

    @Test func testBuildAddAll() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: """
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    ADD . .
                    RUN cat emptyFile
                    RUN cat Test/testempty
                    """,
                context: [
                    .directory("Test"),
                    .file("Test/testempty", content: .zeroFilled(size: 1)),
                    .file("emptyFile", content: .zeroFilled(size: 1)),
                ])
            let image = "registry.local/add-all:\(UUID().uuidString)"
            let output = try f.build(tag: image, contextDir: dir)
            #expect(output.contains(image))
            try f.assertImageBuilt(image)
        }
    }

    @Test func testBuildArg() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: "ARG TAG=unknown\nFROM ghcr.io/linuxcontainers/alpine:${TAG}")
            let image = "registry.local/build-arg:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir, buildArgs: ["TAG=3.20"])
            try f.assertImageBuilt(image)
        }
    }

    @Test func testBuildSecret() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: """
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    RUN --mount=type=secret,id=ENV1 \\
                        --mount=type=secret,id=env2 \\
                        --mount=type=secret,id=env3 \\
                        test xyyzzz = "`cat /run/secrets/ENV1 /run/secrets/env2 /run/secrets/env3`"
                    RUN --mount=type=secret,id=file \\
                        awk 'BEGIN {for(i=0; i<17; i++) for(c=0; c<256; c++) printf("%c", c)}' > /tmp/foo && \\
                        cmp /tmp/foo /run/secrets/file && \\
                        rm /tmp/foo
                    RUN --mount=type=secret,id=empty \\
                        ! test -e /run/secrets/file && \\
                        test -e /run/secrets/empty && \\
                        cmp /dev/null /run/secrets/empty
                    """)

            setenv("ENV1", "x", 1)
            setenv("ENV_VAR", "yy", 1)
            setenv("env3", "zzz", 1)
            f.addCleanup {
                unsetenv("ENV1")
                unsetenv("ENV_VAR")
                unsetenv("env3")
            }

            let testData = Data((0..<17).flatMap { _ in Array(0...255) })
            let secretFile = try f.createTempFile(suffix: " _f,i=l.e+ ", contents: testData)
            let emptyFile = try f.createTempFile(suffix: "file2", contents: Data())

            let image = "registry.local/secrets:\(UUID().uuidString)"
            try f.build(
                tag: image, contextDir: dir,
                otherArgs: [
                    "--secret", "id=ENV1",
                    "--secret", "id=env2,env=ENV_VAR",
                    "--secret", "id=env3,env=env3",
                    "--secret", "id=file,src=\(secretFile.string)",
                    "--secret", "id=empty,src=\(emptyFile.string)",
                ])
            try f.assertImageBuilt(image)
        }
    }

    @Test func testBuildNetworkAccess() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: """
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    ARG HTTP_PROXY
                    ARG HTTPS_PROXY
                    ARG NO_PROXY
                    ARG http_proxy
                    ARG https_proxy
                    ARG no_proxy
                    RUN apk add --no-cache curl
                    """)
            var buildArgs: [String] = []
            for key in ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"] {
                if let v = ProcessInfo.processInfo.environment[key] { buildArgs.append("\(key)=\(v)") }
            }
            let image = "registry.local/build-network-access:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir, buildArgs: buildArgs)
            try f.assertImageBuilt(image)
        }
    }

    @Test func testBuildDockerfileKeywords() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: """
                    ARG TAG=3.20
                    FROM ghcr.io/linuxcontainers/alpine:${TAG}
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    RUN echo "Hello, World!" > /hello.txt
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    RUN ["sh", "-c", "echo 'Exec form' > /exec.txt"]
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    CMD ["echo", "Exec default"]
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    LABEL version="1.0" description="Test image"
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    EXPOSE 8080
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    ENV MY_ENV=hello
                    RUN echo $MY_ENV > /env.txt
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    ADD emptyFile /
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    COPY toCopy /toCopy
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    ENTRYPOINT ["echo", "entrypoint!"]
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    VOLUME /data
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    RUN adduser -D myuser
                    USER myuser
                    CMD whoami
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    WORKDIR /app
                    RUN pwd > /pwd.out
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    ARG MY_VAR=default
                    RUN echo $MY_VAR > /var.out
                    """,
                context: [
                    .file("emptyFile", content: .zeroFilled(size: 1)),
                    .file("toCopy", content: .zeroFilled(size: 1)),
                ])
            let image = "registry.local/dockerfile-keywords:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir)
            try f.assertImageBuilt(image)
        }
    }

    @Test func testBuildSymlink() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            let dockerfile = """
                FROM ghcr.io/linuxcontainers/alpine:3.20
                ADD Test1Source Test1Source
                ADD Test1Source2 Test1Source2
                RUN cat Test1Source2/test.yaml
                FROM ghcr.io/linuxcontainers/alpine:3.20
                ADD Test2Source Test2Source
                ADD Test2Source2 Test2Source2
                RUN cat Test2Source2/Test/test.txt
                FROM ghcr.io/linuxcontainers/alpine:3.20
                ADD Test3Source Test3Source
                ADD Test3Source2 Test3Source2
                RUN cat Test3Source2/Dest/test.txt
                """
            let context: [FSEntry] = [
                .directory("Test1Source"), .directory("Test1Source2"),
                .file("Test1Source/test.yaml", content: .zeroFilled(size: 200)),
                .symbolicLink("Test1Source2/test.yaml", target: "Test1Source/test.yaml"),
                .directory("Test2Source"), .directory("Test2Source2"),
                .file("Test2Source/Test/Test/test.yaml", content: .zeroFilled(size: 300)),
                .symbolicLink("Test2Source2/Test/test.yaml", target: "Test2Source/Test/Test/test.yaml"),
                .directory("Test3Source/Source"), .directory("Test3Source2"),
                .file("Test3Source/Source/test.txt", content: .zeroFilled(size: 1)),
                .symbolicLink("Test3Source2/Dest", target: "Test3Source/Source"),
            ]
            try f.createContext(dir: dir, dockerfile: dockerfile, context: context)
            let image = "registry.local/build-symlinks:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir)
            try f.assertImageBuilt(image)
        }
    }

    @Test func testBuildAndRun() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: "FROM ghcr.io/linuxcontainers/alpine:3.20\nRUN echo \"foobar\" > /file")
            let image = "\(f.testID)-build-and-run:latest"
            try f.build(tag: image, contextDir: dir)
            try f.assertImageBuilt(image)
            try await f.withContainer(image: image) { name in
                let output = try f.doExec(name, cmd: ["cat", "/file"])
                    .trimmingCharacters(in: .whitespacesAndNewlines)
                #expect(output == "foobar")
            }
        }
    }

    @Test func testBuildDifferentPaths() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: """
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    RUN ls ./
                    COPY . /root
                    RUN cat /root/Test/test.txt
                    """,
                context: [
                    .directory(".git"),
                    .file(".git/FETCH", content: .zeroFilled(size: 1)),
                    .directory("Test"),
                    .file("Test/test.txt", content: .zeroFilled(size: 1)),
                ])
            let image = "registry.local/build-diff-context:\(UUID().uuidString)"
            try f.buildWithPaths(tags: [image], contextDir: dir)
            try f.assertImageBuilt(image)
        }
    }

    @Test func testBuildMultiArch() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: """
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    ADD . .
                    RUN cat emptyFile
                    RUN cat Test/testempty
                    """,
                context: [
                    .directory("Test"),
                    .file("Test/testempty", content: .zeroFilled(size: 1)),
                    .file("emptyFile", content: .zeroFilled(size: 1)),
                ])
            let image = "registry.local/multi-arch:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir, otherArgs: ["--arch", "amd64,arm64"])
            try f.assertImageBuilt(image)

            let output = try f.doInspectImages(image)
            #expect(output.count == 1, "expected single inspect result")
            let archs = Set(output[0].variants.map { $0.platform.architecture })
            #expect(archs == Set(["amd64", "arm64"]), "expected amd64 and arm64 variants")
        }
    }

    @Test func testBuildMultipleTags() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: "FROM scratch\nADD emptyFile /",
                context: [.file("emptyFile", content: .zeroFilled(size: 1))])
            let uuid = UUID().uuidString
            let tag1 = "registry.local/multi-tag-test:\(uuid)"
            let tag2 = "registry.local/multi-tag-test:latest"
            let tag3 = "registry.local/multi-tag-test:v1.0.0"
            let output = try f.buildWithPaths(tags: [tag1, tag2, tag3], contextDir: dir)
            #expect(output.contains(tag1))
            #expect(output.contains(tag2))
            #expect(output.contains(tag3))
            try f.assertImageBuilt(tag1)
            try f.assertImageBuilt(tag2)
            try f.assertImageBuilt(tag3)
        }
    }

    @Test func testBuildAfterContextChange() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            let initialContent = "initial".data(using: .utf8)!
            try f.createContext(
                dir: dir,
                dockerfile: "FROM ghcr.io/linuxcontainers/alpine:3.20\nCOPY foo /foo\nCOPY bar /bar",
                context: [
                    .file("foo", content: .data(Data((0..<4 * 1024 * 1024).map { UInt8($0 % 256) }))),
                    .file("bar", content: .data(initialContent)),
                ])

            let image1 = "\(f.testID)-build-context-change:v1"
            try f.build(tag: image1, contextDir: dir)
            try await f.withContainer(image: image1) { name in
                let out = try f.doExec(name, cmd: ["cat", "/bar"])
                #expect(out == "initial")
            }

            let contextBar = dir.appending("context").appending("bar")
            try "updated".data(using: .utf8)!.write(to: URL(filePath: contextBar.string), options: .atomic)

            let image2 = "\(f.testID)-build-context-change:v2"
            try f.build(tag: image2, contextDir: dir)
            try await f.withContainer(image: image2) { name in
                let out = try f.doExec(name, cmd: ["cat", "/bar"])
                #expect(out == "updated")
            }
        }
    }

    @Test func testBuildWithDockerfileFromStdin() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            let dockerfile = "FROM scratch\nADD emptyFile /"
            try f.createContext(
                dir: dir, dockerfile: "",
                context: [.file("emptyFile", content: .zeroFilled(size: 1))])
            let image = "registry.local/stdin-file:\(UUID().uuidString)"
            try f.buildWithStdin(tags: [image], contextDir: dir, dockerfileContents: dockerfile)
            try f.assertImageBuilt(image)
        }
    }

    @Test func testLowercaseDockerfile() async throws {
        try await ContainerFixture.with { f in
            let files: [(String, String, String)] = [
                ("COPY . /app", "copy-uppercase", "COPY"),
                ("copy . /app", "copy-lowercase", "copy"),
                ("ADD . /app", "add-uppercase", "ADD"),
                ("add . /app", "add-lowercase", "add"),
            ]
            for (instruction, name, _) in files {
                let dir = try f.createTempDir()
                try f.createContext(
                    dir: dir,
                    dockerfile: """
                        FROM ghcr.io/linuxcontainers/alpine:3.20
                        \(instruction)
                        RUN test -f /app/testfile.txt
                        """,
                    context: [.file("testfile.txt", content: .data("test".data(using: .utf8)!))])
                let image = "registry.local/\(name):\(UUID().uuidString)"
                try f.build(tag: image, contextDir: dir)
                try f.assertImageBuilt(image)
            }
        }
    }

    @Test func testRunWithBindMount() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: """
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    RUN --mount=type=bind,source=.,target=/mnt/context \\
                        set -e; \\
                        if [ ! -f /mnt/context/app.py ]; then echo "ERROR: app.py missing"; exit 1; fi; \\
                        if [ ! -f /mnt/context/config.yaml ]; then echo "ERROR: config.yaml missing"; exit 1; fi; \\
                        cp /mnt/context/app.py /app.py
                    RUN cat /app.py
                    """,
                context: [
                    .file("app.py", content: .data("print('Hello from bind mount')".data(using: .utf8)!)),
                    .file("config.yaml", content: .data("key: value".data(using: .utf8)!)),
                ])
            let image = "registry.local/bind-mount-test:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir)
            try f.assertImageBuilt(image)
        }
    }

    // MARK: - .dockerignore tests

    @Test func testBuildDockerIgnore() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            let dockerignore = """
                secret.txt
                *.log
                **/*.log
                !important.log
                *.tmp
                **/*.tmp
                temp/
                node_modules/
                """
            try f.createContext(
                dir: dir,
                dockerfile: """
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    COPY . /app
                    RUN set -e; [ ! -f /app/secret.txt ] || exit 1
                    RUN set -e; [ ! -f /app/debug.log ] || exit 1
                    RUN set -e; [ -f /app/important.log ] || exit 1
                    RUN set -e; find /app -name "*.tmp" | grep . && exit 1; true
                    RUN set -e; [ ! -d /app/temp ] || exit 1
                    RUN set -e; [ ! -d /app/node_modules ] || exit 1
                    RUN set -e; [ -f /app/main.go ] && [ -f /app/README.md ] && [ -f /app/src/app.go ]
                    """,
                context: [
                    .file(".dockerignore", content: .data(dockerignore.data(using: .utf8)!)),
                    .file("secret.txt", content: .data("secret".data(using: .utf8)!)),
                    .file("debug.log", content: .data("debug".data(using: .utf8)!)),
                    .file("important.log", content: .data("important".data(using: .utf8)!)),
                    .file("cache.tmp", content: .data("cache".data(using: .utf8)!)),
                    .file("main.go", content: .data("package main".data(using: .utf8)!)),
                    .file("README.md", content: .data("# README".data(using: .utf8)!)),
                    .directory("temp"),
                    .file("logs/app.log", content: .data("app log".data(using: .utf8)!)),
                    .directory("node_modules"),
                    .directory("src"),
                    .file("src/app.go", content: .data("package src".data(using: .utf8)!)),
                ])
            let image = "registry.local/dockerignore-test:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir)
            try f.assertImageBuilt(image)
        }
    }

    @Test func testDockerIgnoreBasic() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            let dockerfile = "FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . ."
            try f.createContext(
                dir: dir,
                dockerfile: dockerfile,
                context: [
                    .file("Dockerfile", content: .data(dockerfile.data(using: .utf8)!)),
                    .file("included.txt", content: .data("included\n".data(using: .utf8)!)),
                    .file("ignored.txt", content: .data("ignored\n".data(using: .utf8)!)),
                    .file(".dockerignore", content: .data("ignored.txt\n".data(using: .utf8)!)),
                ])
            let contextDir = dir.appending("context")
            let image = "registry.local/dockerignore-basic:\(UUID().uuidString)"
            let result = try f.run([
                "build", "-f", contextDir.appending("Dockerfile").string,
                "-t", image, contextDir.string,
            ])
            try result.check()
            try await f.withContainer(image: image, tag: "c") { name in
                try f.assertContainerHasFile(name, at: "/app/included.txt")
                try f.assertContainerMissingFile(name, at: "/app/ignored.txt")
            }
        }
    }

    @Test func testDockerIgnoreDockerfileSpecific() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            let dockerfile = "FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . ."
            try f.createContext(
                dir: dir, dockerfile: dockerfile,
                context: [
                    .file("Dockerfile", content: .data(dockerfile.data(using: .utf8)!)),
                    .file(".dockerignore", content: .data("general.txt\n".data(using: .utf8)!)),
                    .file("Dockerfile.dockerignore", content: .data("specific.txt\n".data(using: .utf8)!)),
                    .file("general.txt", content: .data("general\n".data(using: .utf8)!)),
                    .file("specific.txt", content: .data("specific\n".data(using: .utf8)!)),
                ])
            let contextDir = dir.appending("context")
            let image = "registry.local/dockerignore-specific:\(UUID().uuidString)"
            try f.run([
                "build", "-f", contextDir.appending("Dockerfile").string,
                "-t", image, contextDir.string,
            ]).check()
            try await f.withContainer(image: image, tag: "c") { name in
                try f.assertContainerMissingFile(name, at: "/app/specific.txt", "specific.txt should be ignored by Dockerfile.dockerignore")
                try f.assertContainerHasFile(name, at: "/app/general.txt", "general.txt should be present (Dockerfile.dockerignore takes precedence)")
            }
        }
    }

    @Test func testDockerIgnoreOutsideContext() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            let dockerfile = "FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . ."
            try f.createContext(
                dir: dir, dockerfile: dockerfile,
                context: [
                    .file(".dockerignore", content: .data("general.txt\n".data(using: .utf8)!)),
                    .file("general.txt", content: .data("general\n".data(using: .utf8)!)),
                    .file("specific.txt", content: .data("specific\n".data(using: .utf8)!)),
                ])
            try "specific.txt\n".data(using: .utf8)!.write(to: URL(filePath: dir.appending("Dockerfile.dockerignore").string), options: .atomic)
            let image = "registry.local/dockerignore-outside:\(UUID().uuidString)"
            try f.run([
                "build", "-f", dir.appending("Dockerfile").string,
                "-t", image, dir.appending("context").string,
            ]).check()
            try await f.withContainer(image: image, tag: "c") { name in
                try f.assertContainerMissingFile(name, at: "/app/specific.txt")
                try f.assertContainerHasFile(name, at: "/app/general.txt")
            }
        }
    }

    @Test func testDockerIgnoreIgnoredDockerfile() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            let dockerfile = "FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . ."
            try f.createContext(
                dir: dir, dockerfile: dockerfile,
                context: [
                    .file("Dockerfile", content: .data(dockerfile.data(using: .utf8)!)),
                    .file(".dockerignore", content: .data("Dockerfile\n.dockerignore\n".data(using: .utf8)!)),
                    .file("test.txt", content: .data("test\n".data(using: .utf8)!)),
                ])
            let contextDir = dir.appending("context")
            let image = "registry.local/dockerignore-ignored-dockerfile:\(UUID().uuidString)"
            try f.run([
                "build", "-f", contextDir.appending("Dockerfile").string,
                "-t", image, contextDir.string,
            ]).check()
            try await f.withContainer(image: image, tag: "c") { name in
                try f.assertContainerMissingFile(name, at: "/app/Dockerfile")
                try f.assertContainerMissingFile(name, at: "/app/.dockerignore")
                try f.assertContainerHasFile(name, at: "/app/test.txt")
            }
        }
    }

    @Test func testDockerIgnoreSubdirDockerfile() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            let dockerfile = "FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . ."
            try f.createContext(
                dir: dir, dockerfile: dockerfile,
                context: [
                    .file(".dockerignore", content: .data("included.txt\n".data(using: .utf8)!)),
                    .file("included.txt", content: .data("included\n".data(using: .utf8)!)),
                    .file("secret.txt", content: .data("secret\n".data(using: .utf8)!)),
                    .file("nested/secret.txt", content: .data("nested secret\n".data(using: .utf8)!)),
                    .file("nested/project/Dockerfile", content: .data(dockerfile.data(using: .utf8)!)),
                    .file("nested/project/Dockerfile.dockerignore", content: .data("secret.txt\n**/secret.txt\n".data(using: .utf8)!)),
                    .file("nested/project/config.txt", content: .data("config\n".data(using: .utf8)!)),
                ])
            let contextDir = dir.appending("context")
            let nestedDockerfile = contextDir.appending("nested").appending("project").appending("Dockerfile")
            let image = "registry.local/dockerignore-subdir:\(UUID().uuidString)"
            try f.run([
                "build", "-f", nestedDockerfile.string, "-t", image, contextDir.string,
            ]).check()
            try await f.withContainer(image: image, tag: "c") { name in
                try f.assertContainerHasFile(name, at: "/app/included.txt")
                try f.assertContainerMissingFile(name, at: "/app/secret.txt")
                try f.assertContainerMissingFile(name, at: "/app/nested/secret.txt")
                try f.assertContainerHasFile(name, at: "/app/nested/project/config.txt")
            }
        }
    }

    @Test func testDockerIgnoreCustomDockerfileName() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            let dockerfile = "FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . ."
            try f.createContext(
                dir: dir, dockerfile: "",  // no top-level Dockerfile
                context: [
                    .file(".dockerignore", content: .data("generic.txt\n".data(using: .utf8)!)),
                    .file("app1.Dockerfile", content: .data(dockerfile.data(using: .utf8)!)),
                    .file("app1.Dockerfile.dockerignore", content: .data("app1-specific.txt\n".data(using: .utf8)!)),
                    .file("app1-specific.txt", content: .data("app1 specific\n".data(using: .utf8)!)),
                    .file("generic.txt", content: .data("generic\n".data(using: .utf8)!)),
                    .file("included.txt", content: .data("included\n".data(using: .utf8)!)),
                ])
            let contextDir = dir.appending("context")
            let image = "registry.local/dockerignore-custom-name:\(UUID().uuidString)"
            try f.run([
                "build", "-f", contextDir.appending("app1.Dockerfile").string,
                "-t", image, contextDir.string,
            ]).check()
            try await f.withContainer(image: image, tag: "c") { name in
                try f.assertContainerMissingFile(name, at: "/app/app1-specific.txt")
                try f.assertContainerHasFile(name, at: "/app/generic.txt")
                try f.assertContainerHasFile(name, at: "/app/included.txt")
            }
        }
    }

    @Test func testDockerIgnoreCustomNameSubdir() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            let dockerfile = "FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . ."
            try f.createContext(
                dir: dir, dockerfile: "",
                context: [
                    .file(".dockerignore", content: .data("from-root-ignore.txt\n".data(using: .utf8)!)),
                    .file("from-root-ignore.txt", content: .data("root ignore\n".data(using: .utf8)!)),
                    .file("from-app2-ignore.txt", content: .data("app2 ignore\n".data(using: .utf8)!)),
                    .file("always-included.txt", content: .data("always\n".data(using: .utf8)!)),
                    .file("nested/project/app2.Dockerfile", content: .data(dockerfile.data(using: .utf8)!)),
                    .file("nested/project/app2.Dockerfile.dockerignore", content: .data("from-app2-ignore.txt\n".data(using: .utf8)!)),
                    .file("nested/project/config.yaml", content: .data("config\n".data(using: .utf8)!)),
                ])
            let contextDir = dir.appending("context")
            let nestedDockerfile = contextDir.appending("nested").appending("project").appending("app2.Dockerfile")
            let image = "registry.local/dockerignore-custom-subdir:\(UUID().uuidString)"
            try f.run([
                "build", "-f", nestedDockerfile.string, "-t", image, contextDir.string,
            ]).check()
            try await f.withContainer(image: image, tag: "c") { name in
                try f.assertContainerMissingFile(name, at: "/app/from-app2-ignore.txt")
                try f.assertContainerHasFile(name, at: "/app/from-root-ignore.txt")
                try f.assertContainerHasFile(name, at: "/app/always-included.txt")
                try f.assertContainerHasFile(name, at: "/app/nested/project/config.yaml")
            }
        }
    }

    @Test func testDockerIgnoreCoexistingDockerfiles() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            let appDockerfile = "FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . ."
            try f.createContext(
                dir: dir, dockerfile: "",
                context: [
                    .file("Dockerfile", content: .data("FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . .\n".data(using: .utf8)!)),
                    .file("Dockerfile.dockerignore", content: .data("dockerfile-specific.txt\n".data(using: .utf8)!)),
                    .file("app.Dockerfile", content: .data(appDockerfile.data(using: .utf8)!)),
                    .file("app.Dockerfile.dockerignore", content: .data("app-specific.txt\n".data(using: .utf8)!)),
                    .file("dockerfile-specific.txt", content: .data("df specific\n".data(using: .utf8)!)),
                    .file("app-specific.txt", content: .data("app specific\n".data(using: .utf8)!)),
                    .file("included.txt", content: .data("included\n".data(using: .utf8)!)),
                ])
            let contextDir = dir.appending("context")
            let image = "registry.local/dockerignore-coexisting:\(UUID().uuidString)"
            try f.run([
                "build", "-f", contextDir.appending("app.Dockerfile").string,
                "-t", image, contextDir.string,
            ]).check()
            try await f.withContainer(image: image, tag: "c") { name in
                try f.assertContainerMissingFile(name, at: "/app/app-specific.txt")
                try f.assertContainerHasFile(name, at: "/app/dockerfile-specific.txt")
                try f.assertContainerHasFile(name, at: "/app/included.txt")
            }
        }
    }

    @Test func testDockerIgnoreReadonlyContext() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            let dockerfile = "FROM ghcr.io/linuxcontainers/alpine:3.20\nWORKDIR /app\nCOPY . ."
            try f.createContext(
                dir: dir, dockerfile: dockerfile,
                context: [
                    .file("included.txt", content: .data("included\n".data(using: .utf8)!)),
                    .file("secret.txt", content: .data("secret\n".data(using: .utf8)!)),
                ])
            try "secret.txt\n".data(using: .utf8)!.write(to: URL(filePath: dir.appending("Dockerfile.dockerignore").string), options: .atomic)

            let contextDir = dir.appending("context")
            // Make the context read-only, then restore before cleanup.
            try FileManager.default.setAttributes(
                [.posixPermissions: 0o555], ofItemAtPath: contextDir.string)
            f.addCleanup {
                try? FileManager.default.setAttributes(
                    [.posixPermissions: 0o755], ofItemAtPath: contextDir.string)
            }

            let image = "registry.local/dockerignore-readonly:\(UUID().uuidString.prefix(6))"
            try f.run([
                "build", "-f", dir.appending("Dockerfile").string,
                "-t", image, contextDir.string,
            ]).check()
            try await f.withContainer(image: image, tag: "c") { name in
                try f.assertContainerHasFile(name, at: "/app/included.txt")
                try f.assertContainerMissingFile(name, at: "/app/secret.txt")
            }
        }
    }

    @Test func testNonExistingDockerfile() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            let image = "registry.local/non-existing-dockerfile:\(UUID().uuidString)"
            let r1 = try f.run(["build", "-f", "non-existing-path", "-t", image, dir.string])
            #expect(r1.status != 0)
            let r2 = try f.run(["build", "-t", image, dir.string])
            #expect(r2.status != 0)
        }
    }

    // MARK: - Dockerfile ARG quoting

    @Test func testBuildQuotedImageDockerfileArg() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: "ARG IMAGE=\"ghcr.io/linuxcontainers/alpine:3.20\"\nFROM $IMAGE\nRUN test -f /etc/alpine-release")
            let image = "registry.local/quoted-image-dockerfile-arg:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir)
            try f.assertImageBuilt(image)
        }
    }

    @Test func testBuildQuotedStringDockerfileArg() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: "FROM ghcr.io/linuxcontainers/alpine:3.20\nARG MYSTRING='\"Hello, world!\"'\nRUN test \"$MYSTRING\" = '\"Hello, world!\"'")
            let image = "registry.local/quoted-string-dockerfile-arg:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir)
            try f.assertImageBuilt(image)
        }
    }

    @Test func testBuildForwardReferencedDockerfileArg() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: """
                    ARG ALPINE="ghcr.io/linuxcontainers/alpine"
                    ARG IMAGE="${ALPINE}:3.20"
                    FROM $IMAGE
                    RUN test -f /etc/alpine-release
                    """)
            let image = "registry.local/forward-referenced-dockerfile-arg:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir)
            try f.assertImageBuilt(image)
        }
    }

    @Test func testBuildQuotedImageBuildArg() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: "ARG IMAGE\nFROM $IMAGE\nRUN test -f /etc/alpine-release")
            let image = "registry.local/quoted-image-build-arg:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir, buildArgs: ["IMAGE=ghcr.io/linuxcontainers/alpine:3.20"])
            try f.assertImageBuilt(image)
        }
    }

    @Test func testBuildQuotedStringBuildArg() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: "FROM ghcr.io/linuxcontainers/alpine:3.20\nARG MYSTRING\nRUN test \"$MYSTRING\" = '\"Hello, world!\"'")
            let image = "registry.local/quoted-string-build-arg:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir, buildArgs: ["MYSTRING=\"Hello, world!\""])
            try f.assertImageBuilt(image)
        }
    }

    @Test func testBuildForwardReferencedBuildArg() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: """
                    ARG ALPINE
                    ARG IMAGE="$ALPINE:3.20"
                    FROM $IMAGE
                    RUN test -f /etc/alpine-release
                    """)
            let image = "registry.local/forward-referenced-build-arg:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir, buildArgs: ["ALPINE=ghcr.io/linuxcontainers/alpine"])
            try f.assertImageBuilt(image)
        }
    }

    // MARK: - COPY --from tests

    @Test func testCopyFromLocalImage() async throws {
        try await ContainerFixture.with { f in
            let baseDir = try f.createTempDir()
            let baseName = "local-base:\(UUID().uuidString)"
            try f.createContext(
                dir: baseDir,
                dockerfile: "FROM scratch\nADD hello.txt /hello.txt",
                context: [.file("hello.txt", content: .data("hello\n".data(using: .utf8)!))])
            try f.build(tag: baseName, contextDir: baseDir)
            try f.assertImageBuilt(baseName)

            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: "FROM ghcr.io/linuxcontainers/alpine:3.20\nCOPY --from=\(baseName) /hello.txt /copied.txt\nRUN cat /copied.txt")
            let image = "registry.local/copy-from-local:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir)
            try f.assertImageBuilt(image)
        }
    }

    @Test func testCopyFromBuildStage() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: """
                    FROM scratch AS builder
                    ADD hello.txt /hello.txt
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    COPY --from=builder /hello.txt /copied.txt
                    RUN cat /copied.txt
                    """,
                context: [.file("hello.txt", content: .data("hello\n".data(using: .utf8)!))])
            let image = "registry.local/copy-from-stage:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir)
            try f.assertImageBuilt(image)
        }
    }

    @Test func testCopyRenameFromStage() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: """
                    FROM scratch AS builder
                    ADD hello.txt /hello.txt
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    COPY --from=builder /hello.txt /renamed.txt
                    RUN cat /renamed.txt
                    """,
                context: [.file("hello.txt", content: .data("hello\n".data(using: .utf8)!))])
            let image = "registry.local/copy-rename:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir)
            try f.assertImageBuilt(image)
        }
    }

    @Test func testCopyMissingFileFails() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: """
                    FROM scratch AS builder
                    FROM ghcr.io/linuxcontainers/alpine:3.20
                    COPY --from=builder /does-not-exist.txt /copied.txt
                    """)
            let image = "registry.local/copy-missing:\(UUID().uuidString)"
            let result = try f.run([
                "build", "-f", dir.appending("Dockerfile").string,
                "-t", image, dir.appending("context").string,
            ])
            #expect(result.status != 0, "build should fail when source file is missing")
        }
    }

    @Test func testCopyInvalidStageFails() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: "FROM ghcr.io/linuxcontainers/alpine:3.20\nCOPY --from=not_a_stage /hello.txt /copied.txt")
            let image = "registry.local/copy-invalid-stage:\(UUID().uuidString)"
            let result = try f.run([
                "build", "-f", dir.appending("Dockerfile").string,
                "-t", image, dir.appending("context").string,
            ])
            #expect(result.status != 0, "build should fail with invalid stage name")
        }
    }

    @Test func testCopyFromNonexistentImageFails() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            try f.createContext(
                dir: dir,
                dockerfile: "FROM ghcr.io/linuxcontainers/alpine:3.20\nCOPY --from=doesnotexist:latest /hello.txt /copied.txt")
            let image = "registry.local/copy-bad-image:\(UUID().uuidString)"
            let result = try f.run([
                "build", "-f", dir.appending("Dockerfile").string,
                "-t", image, dir.appending("context").string,
            ])
            #expect(result.status != 0, "build should fail when source image does not exist")
        }
    }

    /// Regression test: the context *root* itself (not an entry inside it) is a
    /// symlink to a sibling directory, e.g. `context -> real-context`. The build
    /// must resolve the symlink and use the real directory's contents.
    @Test func testBuildContextRootSymlink() async throws {
        try await ContainerFixture.with { f in
            let dir = try f.createTempDir()
            let dockerfile = """
                FROM ghcr.io/linuxcontainers/alpine:3.20
                COPY hello.txt hello.txt
                RUN cat hello.txt
                """
            try f.createContext(
                dir: dir,
                dockerfile: dockerfile,
                context: [
                    .file("hello.txt", content: .data(Data("hello from real-context".utf8)))
                ])

            let contextPath = dir.appending("context")
            let realContextPath = dir.appending("real-context")
            try FileManager.default.moveItem(atPath: contextPath.string, toPath: realContextPath.string)
            try FileManager.default.createSymbolicLink(
                atPath: contextPath.string, withDestinationPath: "real-context")

            let image = "registry.local/build-context-root-symlink:\(UUID().uuidString)"
            try f.build(tag: image, contextDir: dir)
            try f.assertImageBuilt(image)
        }
    }
}
