//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// 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 Foundation
import Testing

@testable import ContainerBuildIR

struct OperationTests {

    // MARK: - ExecOperation Tests

    @Test func execOperationBasic() throws {
        let operation = ExecOperation(
            command: .shell("echo 'Hello, World!'")
        )

        #expect(operation.operationKind == .exec)
        #expect(operation.command.displayString == "echo 'Hello, World!'")
        #expect(operation.environment.variables.isEmpty)
        #expect(operation.mounts.isEmpty)
        #expect(operation.workingDirectory == nil)
        #expect(operation.user == nil)
        #expect(operation.network == .default)
        #expect(!operation.security.privileged)
    }

    @Test func execOperationWithEnvironment() throws {
        let environment = Environment([
            (key: "NODE_ENV", value: .literal("production")),
            (key: "PORT", value: .buildArg("HTTP_PORT")),
            (key: "DEBUG", value: .literal("false")),
        ])

        let operation = ExecOperation(
            command: .exec(["node", "server.js"]),
            environment: environment
        )

        #expect(operation.environment.variables.count == 3)

        let effectiveEnv = operation.environment.effectiveEnvironment
        #expect(effectiveEnv["NODE_ENV"] == "production")
        #expect(effectiveEnv["DEBUG"] == "false")
        // BUILD_ARG references don't resolve without context
    }

    @Test func execOperationWithMounts() throws {
        let mounts = [
            Mount(
                type: .cache,
                target: "/var/cache/apt",
                source: .local("apt-cache"),
                options: MountOptions(sharing: .shared)
            ),
            Mount(
                type: .secret,
                target: "/run/secrets/github-token",
                source: .secret("github-token"),
                options: MountOptions(readOnly: true, mode: 0o400)
            ),
            Mount(
                type: .tmpfs,
                target: "/tmp",
                source: nil,
                options: MountOptions(size: 100 * 1024 * 1024)  // 100MB
            ),
        ]

        let operation = ExecOperation(
            command: .shell("apt-get update && apt-get install -y git"),
            mounts: mounts
        )

        #expect(operation.mounts.count == 3)

        let cacheMount = operation.mounts[0]
        #expect(cacheMount.type == .cache)
        #expect(cacheMount.target == "/var/cache/apt")
        #expect(cacheMount.options.sharing == .shared)

        let secretMount = operation.mounts[1]
        #expect(secretMount.type == .secret)
        #expect(secretMount.options.readOnly == true)
        #expect(secretMount.options.mode == 0o400)

        let tmpfsMount = operation.mounts[2]
        #expect(tmpfsMount.type == .tmpfs)
        #expect(tmpfsMount.options.size == UInt32(100 * 1024 * 1024))
    }

    @Test func execOperationWithUser() throws {
        let users = [
            User.named("appuser"),
            User.uid(1000),
            User.userGroup(user: "app", group: "app"),
            User.uidGid(uid: 1000, gid: 1000),
        ]

        for user in users {
            let operation = ExecOperation(
                command: .shell("whoami"),
                user: user
            )

            #expect(operation.user == user)
        }
    }

    @Test func execOperationWithSecurity() throws {
        let capabilities = SecurityCapabilities(
            add: ["NET_ADMIN", "SYS_TIME"],
            drop: ["ALL"]
        )

        let security = SecurityOptions(
            privileged: true,
            capabilities: capabilities,
            seccompProfile: "custom.json",
            apparmorProfile: "custom-profile",
            noNewPrivileges: false
        )

        let operation = ExecOperation(
            command: .shell("mount /dev/sda1 /mnt"),
            security: security
        )

        #expect(operation.security.privileged == true)
        #expect(operation.security.capabilities?.add.contains("NET_ADMIN") == true)
        #expect(operation.security.capabilities?.drop.contains("ALL") == true)
        #expect(operation.security.seccompProfile == "custom.json")
        #expect(operation.security.apparmorProfile == "custom-profile")
        #expect(operation.security.noNewPrivileges == false)
    }

    @Test func execOperationWithNetwork() throws {
        let networkModes: [NetworkMode] = [.default, .none, .host]

        for networkMode in networkModes {
            let operation = ExecOperation(
                command: .shell("curl https://example.com"),
                network: networkMode
            )

            #expect(operation.network == networkMode)
        }
    }

    @Test func commandTypes() throws {
        let shellCommand = Command.shell("echo 'test' && ls -la")
        let execCommand = Command.exec(["ls", "-la", "/app"])

        #expect(shellCommand.displayString == "echo 'test' && ls -la")
        #expect(execCommand.displayString == "ls -la /app")

        // Test with empty exec array
        let emptyExecCommand = Command.exec([])
        #expect(emptyExecCommand.displayString == "")

        // Test with single command
        let singleExecCommand = Command.exec(["whoami"])
        #expect(singleExecCommand.displayString == "whoami")
    }

    // MARK: - FilesystemOperation Tests

    @Test func filesystemOperationBasic() throws {
        let operation = FilesystemOperation(
            action: .copy,
            source: .context(ContextSource(paths: ["src/"])),
            destination: "/app/src/"
        )

        #expect(operation.operationKind == .filesystem)
        #expect(operation.action == .copy)
        #expect(operation.destination == "/app/src/")

        if case .context(let contextSource) = operation.source {
            #expect(contextSource.paths == ["src/"])
        } else {
            Issue.record("Expected context source")
        }
    }

    @Test func filesystemOperationFromStage() throws {
        let operation = FilesystemOperation(
            action: .copy,
            source: .stage(.named("builder"), paths: ["/app/dist"]),
            destination: "/usr/share/nginx/html/"
        )

        if case .stage(let stageRef, let paths) = operation.source {
            if case .named(let name) = stageRef {
                #expect(name == "builder")
            } else {
                Issue.record("Expected named stage reference")
            }
            #expect(paths == ["/app/dist"])
        } else {
            Issue.record("Expected stage source")
        }
    }

    @Test func filesystemOperationWithMetadata() throws {
        let expectedOwnership = Ownership(user: .numeric(id: 1000), group: .numeric(id: 1000))
        let fileMetadata = FileMetadata(
            ownership: expectedOwnership,
            permissions: .mode(0o644),
            timestamps: Timestamps(
                created: Date(),
                modified: Date()
            )
        )

        let operation = FilesystemOperation(
            action: .copy,
            source: .context(
                ContextSource(
                    paths: ["config.json"]
                )),
            destination: "/etc/app/config.json",
            fileMetadata: fileMetadata
        )

        #expect(operation.fileMetadata.ownership == expectedOwnership)

        if case .mode(let mode) = operation.fileMetadata.permissions {
            #expect(mode == 0o644)
        } else {
            Issue.record("Expected mode permissions")
        }
    }

    @Test func filesystemSourceTypes() throws {
        let contextSource = FilesystemSource.context(
            ContextSource(
                paths: ["*.txt", "docs/"]
            ))

        let stageSource = FilesystemSource.stage(.index(0), paths: ["/app/binary"])
        let imageSource = FilesystemSource.image(
            ImageReference(parsing: "alpine:latest")!,
            paths: ["/etc/ssl/certs/"]
        )
        let urlSource = FilesystemSource.url(URL(string: "https://releases.example.com/v1.0.0/app.tar.gz")!)

        // Verify each source type can be created
        let sources = [contextSource, stageSource, imageSource, urlSource]
        for source in sources {
            let operation = FilesystemOperation(
                action: .copy,
                source: source,
                destination: "/test/"
            )
            #expect(operation.source == source)
        }
    }

    // MARK: - MetadataOperation Tests

    @Test func metadataOperationEnvironment() throws {
        let setEnvOperation = MetadataOperation(
            action: .setEnv(key: "NODE_ENV", value: .literal("production"))
        )

        #expect(setEnvOperation.operationKind == .metadata)

        if case .setEnv(let key, let value) = setEnvOperation.action {
            #expect(key == "NODE_ENV")
            if case .literal(let literalValue) = value {
                #expect(literalValue == "production")
            } else {
                Issue.record("Expected literal value")
            }
        } else {
            Issue.record("Expected setEnv action")
        }
    }

    @Test func metadataOperationBatchEnvironment() throws {
        let envVars = [
            (key: "NODE_ENV", value: EnvironmentValue.literal("production")),
            (key: "PORT", value: EnvironmentValue.buildArg("HTTP_PORT")),
            (key: "DEBUG", value: EnvironmentValue.literal("false")),
        ]

        let operation = MetadataOperation(
            action: .setEnvBatch(envVars)
        )

        if case .setEnvBatch(let vars) = operation.action {
            #expect(vars.count == 3)
            #expect(vars[0].key == "NODE_ENV")
            #expect(vars[1].key == "PORT")
            #expect(vars[2].key == "DEBUG")
        } else {
            Issue.record("Expected setEnvBatch action")
        }
    }

    @Test func metadataOperationLabels() throws {
        let setLabelOperation = MetadataOperation(
            action: .setLabel(key: "version", value: "1.0.0")
        )

        if case .setLabel(let key, let value) = setLabelOperation.action {
            #expect(key == "version")
            #expect(value == "1.0.0")
        } else {
            Issue.record("Expected setLabel action")
        }

        let batchLabels = [
            "version": "1.0.0",
            "maintainer": "team@example.com",
            "description": "Sample application",
        ]

        let batchLabelOperation = MetadataOperation(
            action: .setLabelBatch(batchLabels)
        )

        if case .setLabelBatch(let labels) = batchLabelOperation.action {
            #expect(labels.count == 3)
            #expect(labels["version"] == "1.0.0")
            #expect(labels["maintainer"] == "team@example.com")
        } else {
            Issue.record("Expected setLabelBatch action")
        }
    }

    @Test func metadataOperationArguments() throws {
        let operation = MetadataOperation(
            action: .declareArg(name: "BUILD_VERSION", defaultValue: "dev")
        )

        if case .declareArg(let name, let defaultValue) = operation.action {
            #expect(name == "BUILD_VERSION")
            #expect(defaultValue == "dev")
        } else {
            Issue.record("Expected declareArg action")
        }
    }

    @Test func metadataOperationExpose() throws {
        let operation = MetadataOperation(
            action: .expose(ports: [PortSpec(port: 8080, protocol: .tcp)])
        )

        if case .expose(let ports) = operation.action {
            #expect(!ports.isEmpty)
            let port = ports[0]
            #expect(port.port == 8080)
            #expect(port.protocol == .tcp)
        } else {
            Issue.record("Expected expose action")
        }
    }

    @Test func metadataOperationWorkdir() throws {
        let operation = MetadataOperation(
            action: .setWorkdir(path: "/app")
        )

        if case .setWorkdir(let path) = operation.action {
            #expect(path == "/app")
        } else {
            Issue.record("Expected setWorkdir action")
        }
    }

    @Test func metadataOperationUser() throws {
        let userOperations = [
            MetadataOperation(action: .setUser(user: .named("appuser"))),
            MetadataOperation(action: .setUser(user: .uid(1000))),
            MetadataOperation(action: .setUser(user: .uidGid(uid: 1000, gid: 1000))),
        ]

        for operation in userOperations {
            if case .setUser(let user) = operation.action {
                // Just verify the user is preserved
                switch user {
                case .named(let name):
                    #expect(name == "appuser")
                case .uid(let uid):
                    #expect(uid == 1000)
                case .uidGid(let uid, let gid):
                    #expect(uid == 1000)
                    #expect(gid == 1000)
                default:
                    Issue.record("Unexpected user type")
                }
            } else {
                Issue.record("Expected setUser action")
            }
        }
    }

    @Test func metadataOperationCommands() throws {
        let entrypointOperation = MetadataOperation(
            action: .setEntrypoint(command: .exec(["/app/server"]))
        )

        let cmdOperation = MetadataOperation(
            action: .setCmd(command: .shell("./start.sh"))
        )

        if case .setEntrypoint(let command) = entrypointOperation.action {
            if case .exec(let args) = command {
                #expect(args == ["/app/server"])
            } else {
                Issue.record("Expected exec command")
            }
        } else {
            Issue.record("Expected setEntrypoint action")
        }

        if case .setCmd(let command) = cmdOperation.action {
            if case .shell(let cmd) = command {
                #expect(cmd == "./start.sh")
            } else {
                Issue.record("Expected shell command")
            }
        } else {
            Issue.record("Expected setCmd action")
        }
    }

    @Test func metadataOperationHealthcheck() throws {
        let healthcheck = Healthcheck(
            test: .command(.exec(["curl", "-f", "http://localhost:8080/health"])),
            interval: 30,
            timeout: 5,
            startPeriod: 10,
            retries: 3
        )

        let operation = MetadataOperation(
            action: .setHealthcheck(healthcheck: healthcheck)
        )

        if case .setHealthcheck(let hc) = operation.action {
            #expect(hc?.interval == 30)
            #expect(hc?.timeout == 5)
            #expect(hc?.startPeriod == 10)
            #expect(hc?.retries == 3)

            if case .command(let cmd) = hc!.test,
                case .exec(let args) = cmd
            {
                #expect(args == ["curl", "-f", "http://localhost:8080/health"])
            } else {
                Issue.record("Expected command healthcheck test")
            }
        } else {
            Issue.record("Expected setHealthcheck action")
        }
    }

    @Test func metadataOperationMiscellaneous() throws {
        let stopSignalOp = MetadataOperation(action: .setStopSignal(signal: "SIGTERM"))
        let volumeOp = MetadataOperation(action: .addVolume(path: "/data"))
        let shellOp = MetadataOperation(action: .setShell(shell: ["/bin/bash", "-c"]))
        let onBuildOp = MetadataOperation(action: .addOnBuild(instruction: "RUN npm install"))

        let operations = [stopSignalOp, volumeOp, shellOp, onBuildOp]

        for operation in operations {
            #expect(operation.operationKind == .metadata)

            switch operation.action {
            case .setStopSignal(let signal):
                #expect(signal == "SIGTERM")
            case .addVolume(let path):
                #expect(path == "/data")
            case .setShell(let shell):
                #expect(shell == ["/bin/bash", "-c"])
            case .addOnBuild(let instruction):
                #expect(instruction == "RUN npm install")
            default:
                Issue.record("Unexpected action type")
            }
        }
    }

    // MARK: - ImageOperation Tests

    @Test func imageOperationFromRegistry() throws {
        let imageRef = ImageReference(parsing: "alpine:3.18")!
        let operation = ImageOperation(
            source: .registry(imageRef),
            platform: .linuxAMD64
        )

        #expect(operation.operationKind == .image)
        #expect(operation.platform == .linuxAMD64)

        if case .registry(let ref) = operation.source {
            #expect(ref.stringValue == "alpine:3.18")
        } else {
            Issue.record("Expected registry source")
        }
    }

    @Test func imageOperationFromScratch() throws {
        let operation = ImageOperation(source: .scratch)

        #expect(operation.platform == nil)  // Default platform

        if case .scratch = operation.source {
            // Expected
        } else {
            Issue.record("Expected scratch source")
        }
    }

    @Test func imageOperationFromOCILayout() throws {
        let operation = ImageOperation(
            source: .ociLayout(path: "/path/to/build/context", tag: "custom"),
            platform: .linuxARM64
        )

        if case .ociLayout(let path, let tag) = operation.source {
            #expect(path == "/path/to/build/context")
            #expect(tag == "custom")
        } else {
            Issue.record("Expected ociLayout source")
        }
    }

    // MARK: - Operation Visitor Pattern Tests

    class TestOperationVisitor: OperationVisitor {
        typealias Result = String

        var visitedOperations: [String] = []

        func visit(_ operation: ExecOperation) throws -> String {
            visitedOperations.append("exec")
            return "Exec: \(operation.command.displayString)"
        }

        func visit(_ operation: FilesystemOperation) throws -> String {
            visitedOperations.append("filesystem")
            return "Filesystem: \(operation.action) to \(operation.destination)"
        }

        func visit(_ operation: MetadataOperation) throws -> String {
            visitedOperations.append("metadata")
            return "Metadata: \(operation.action)"
        }

        func visit(_ operation: ImageOperation) throws -> String {
            visitedOperations.append("image")
            return "Image: \(operation.source)"
        }

        func visitUnknown(_ operation: any ContainerBuildIR.Operation) throws -> String {
            visitedOperations.append("unknown")
            return "Unknown: \(operation.operationKind)"
        }
    }

    @Test func operationVisitorPattern() throws {
        let operations: [any ContainerBuildIR.Operation] = [
            ExecOperation(command: .shell("echo test")),
            FilesystemOperation(
                action: .copy,
                source: .context(ContextSource(paths: ["file.txt"])),
                destination: "/app/"
            ),
            MetadataOperation(action: .setEnv(key: "TEST", value: .literal("value"))),
            ImageOperation(source: .registry(ImageReference(parsing: "alpine")!)),
        ]

        let visitor = TestOperationVisitor()
        var results: [String] = []

        for operation in operations {
            let result = try operation.accept(visitor)
            results.append(result)
        }

        #expect(visitor.visitedOperations == ["exec", "filesystem", "metadata", "image"])
        #expect(results.count == 4)
        #expect(results[0].hasPrefix("Exec:"))
        #expect(results[1].hasPrefix("Filesystem:"))
        #expect(results[2].hasPrefix("Metadata:"))
        #expect(results[3].hasPrefix("Image:"))
    }

    // MARK: - Operation Serialization Tests

    @Test func operationSerialization() throws {
        let operations: [any ContainerBuildIR.Operation] = [
            ExecOperation(
                command: .exec(["npm", "install"]),
                environment: Environment([(key: "NODE_ENV", value: .literal("production"))]),
                user: .uid(1000)
            ),
            FilesystemOperation(
                action: .copy,
                source: .context(ContextSource(paths: ["src/"])),
                destination: "/app/src/",
                fileMetadata: FileMetadata(
                    ownership: Ownership(user: .numeric(id: 1000), group: .numeric(id: 1000)),
                    permissions: .mode(0o755)
                )
            ),
            MetadataOperation(
                action: .setLabel(key: "version", value: "1.0.0")
            ),
            ImageOperation(
                source: .registry(ImageReference(parsing: "node:18")!),
                platform: .linuxAMD64
            ),
        ]

        let encoder = JSONEncoder()
        let decoder = JSONDecoder()

        for operation in operations {
            // Test that each operation type can be encoded and decoded
            let data: Data

            switch operation {
            case let execOp as ExecOperation:
                data = try encoder.encode(execOp)
                let decoded = try decoder.decode(ExecOperation.self, from: data)
                #expect(decoded.command.displayString == execOp.command.displayString)
                #expect(decoded.user == execOp.user)

            case let fsOp as FilesystemOperation:
                data = try encoder.encode(fsOp)
                let decoded = try decoder.decode(FilesystemOperation.self, from: data)
                #expect(decoded.action == fsOp.action)
                #expect(decoded.destination == fsOp.destination)

            case let metaOp as MetadataOperation:
                data = try encoder.encode(metaOp)
                _ = try decoder.decode(MetadataOperation.self, from: data)
            // Note: MetadataAction comparison would need custom implementation

            case let imageOp as ImageOperation:
                data = try encoder.encode(imageOp)
                let decoded = try decoder.decode(ImageOperation.self, from: data)
                #expect(decoded.platform == imageOp.platform)

            default:
                Issue.record("Unexpected operation type")
            }
        }
    }

    // MARK: - Operation Metadata Tests

    @Test func operationMetadata() throws {
        let sourceLocation = SourceLocation(
            file: "Dockerfile",
            line: 15,
            column: 5
        )

        let metadata = OperationMetadata(
            comment: "Install application dependencies",
            sourceLocation: sourceLocation
        )

        let operation = ExecOperation(
            command: .shell("npm install"),
            metadata: metadata
        )

        #expect(operation.metadata.comment == "Install application dependencies")
        #expect(operation.metadata.sourceLocation?.file == "Dockerfile")
        #expect(operation.metadata.sourceLocation?.line == 15)
    }

    // MARK: - Complex Operation Tests

    @Test func complexExecOperation() throws {
        let complexOperation = ExecOperation(
            command: .shell("cd /app && npm ci --only=production && npm run build"),
            environment: Environment([
                (key: "NODE_ENV", value: .literal("production")),
                (key: "BUILD_TARGET", value: .buildArg("TARGET")),
                (key: "CACHE_DIR", value: .literal("/tmp/cache")),
            ]),
            mounts: [
                Mount(
                    type: .cache,
                    target: "/tmp/cache",
                    source: .local("build-cache"),
                    options: MountOptions(sharing: .shared)
                ),
                Mount(
                    type: .secret,
                    target: "/run/secrets/npmrc",
                    source: .secret("npmrc"),
                    options: MountOptions(readOnly: true, mode: 0o600)
                ),
            ],
            workingDirectory: "/app",
            user: .uidGid(uid: 1000, gid: 1000),
            network: .default,
            security: SecurityOptions(
                privileged: false,
                noNewPrivileges: true
            ),
            metadata: OperationMetadata(
                comment: "Build application with caching and secrets"
            )
        )

        #expect(complexOperation.command.displayString.contains("npm ci"))
        #expect(complexOperation.environment.variables.count == 3)
        #expect(complexOperation.mounts.count == 2)
        #expect(complexOperation.workingDirectory == "/app")
        #expect(complexOperation.user != nil)
        #expect(complexOperation.security.noNewPrivileges == true)
        #expect(complexOperation.metadata.comment?.contains("Build application") == true)
    }

    @Test func complexFilesystemOperation() throws {
        let complexOperation = FilesystemOperation(
            action: .copy,
            source: .stage(.named("builder"), paths: ["/app/dist/**/*", "/app/package.json"]),
            destination: "/usr/share/nginx/html/",
            fileMetadata: FileMetadata(
                ownership: Ownership(user: .named(id: "nginx"), group: .named(id: "nginx")),
                permissions: .mode(0o644),
                timestamps: Timestamps(
                    created: Date(),
                    modified: Date()
                )
            ),
            metadata: OperationMetadata(
                comment: "Copy built assets from builder stage",
                sourceLocation: SourceLocation(file: "Dockerfile", line: 25, column: 1)
            )
        )

        if case .stage(let stageRef, let paths) = complexOperation.source {
            if case .named(let name) = stageRef {
                #expect(name == "builder")
            }
            #expect(paths.count == 2)
            #expect(paths.contains("/app/dist/**/*"))
        } else {
            Issue.record("Expected stage source")
        }

        #expect(complexOperation.destination == "/usr/share/nginx/html/")
        // Check fileMetadata exists and has expected values
        #expect(complexOperation.metadata.comment?.contains("built assets") == true)
    }

    // MARK: - Edge Cases and Error Conditions

    @Test func emptyOperations() throws {
        // Test operations with minimal/empty configurations
        let minimalExec = ExecOperation(command: .exec([]))
        #expect(minimalExec.command.displayString == "")
        #expect(minimalExec.mounts.isEmpty)

        let minimalFilesystem = FilesystemOperation(
            action: .copy,
            source: .context(ContextSource(paths: [])),
            destination: ""
        )
        #expect(minimalFilesystem.destination == "")

        if case .context(let ctx) = minimalFilesystem.source {
            #expect(ctx.paths.isEmpty)
        }
    }

    @Test func operationEquality() throws {
        let exec1 = ExecOperation(command: .shell("echo test"))
        let exec2 = ExecOperation(command: .shell("echo test"))
        let exec3 = ExecOperation(command: .shell("echo different"))

        #expect(exec1 == exec2)
        #expect(exec1 != exec3)

        let fs1 = FilesystemOperation(
            action: .copy,
            source: .context(ContextSource(paths: ["file.txt"])),
            destination: "/app/"
        )
        let fs2 = FilesystemOperation(
            action: .copy,
            source: .context(ContextSource(paths: ["file.txt"])),
            destination: "/app/"
        )

        #expect(fs1 == fs2)
    }

    @Test func operationHashing() throws {
        let exec1 = ExecOperation(command: .shell("echo test"))
        let exec2 = ExecOperation(command: .shell("echo different"))
        let fs1 = FilesystemOperation(
            action: .copy,
            source: .context(ContextSource(paths: ["file.txt"])),
            destination: "/app/"
        )

        let operations: Set<AnyHashable> = Set([
            AnyHashable(exec1),
            AnyHashable(exec2),
            AnyHashable(fs1),
        ])

        #expect(operations.count == 3)

        // Test that identical operations hash to the same value
        let exec1_copy = ExecOperation(command: .shell("echo test"))
        let exec2_copy = ExecOperation(command: .shell("echo test"))

        var hasher1 = Hasher()
        var hasher2 = Hasher()

        exec1_copy.hash(into: &hasher1)
        exec2_copy.hash(into: &hasher2)

        #expect(hasher1.finalize() == hasher2.finalize())
    }
}
