//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization 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 ContainerizationOCI
import Foundation
import Testing

@testable import Containerization

struct MountTests {

    @Test func mountShareCreatesVirtiofsMount() {
        let mount = Mount.share(
            source: "/host/shared",
            destination: "/guest/shared",
            options: ["rw", "noatime"],
            runtimeOptions: ["tag=shared"]
        )

        #expect(mount.type == "virtiofs")
        #expect(mount.source == "/host/shared")
        #expect(mount.destination == "/guest/shared")
        #expect(mount.options == ["rw", "noatime"])

        if case .virtiofs(let opts) = mount.runtimeOptions {
            #expect(opts == ["tag=shared"])
        } else {
            #expect(Bool(false), "Expected virtiofs runtime options")
        }
    }

    @Test func sortMountsByDestinationDepthPreventsParentShadowing() {
        let mounts: [ContainerizationOCI.Mount] = [
            .init(destination: "/tmp/foo/bar"),
            .init(destination: "/tmp"),
            .init(destination: "/var/log/app"),
            .init(destination: "/var"),
        ]

        let sorted = sortMountsByDestinationDepth(mounts)

        #expect(
            sorted.map(\.destination) == [
                "/tmp",
                "/var",
                "/tmp/foo/bar",
                "/var/log/app",
            ])
    }

    @Test func sortMountsByDestinationDepthPreservesOrderForEqualDepth() {
        let mounts: [ContainerizationOCI.Mount] = [
            .init(destination: "/b"),
            .init(destination: "/a"),
            .init(destination: "/c"),
        ]

        let sorted = sortMountsByDestinationDepth(mounts)

        // All same depth, order should be preserved (stable sort).
        #expect(sorted.map(\.destination) == ["/b", "/a", "/c"])
    }

    @Test func sortMountsByDestinationDepthHandlesTrailingAndDoubleSlashes() {
        let mounts: [ContainerizationOCI.Mount] = [
            .init(destination: "/a//b/c"),
            .init(destination: "/a/"),
        ]

        let sorted = cleanAndSortMounts(mounts)

        // Paths are cleaned: "/a/" -> "/a", "/a//b/c" -> "/a/b/c"
        #expect(sorted.map(\.destination) == ["/a", "/a/b/c"])
    }

    @Test func sortMountsByDestinationDepthCleansDotAndDotDot() {
        let mounts: [ContainerizationOCI.Mount] = [
            .init(destination: "/tmp/../foo"),
            .init(destination: "/tmp/./bar/baz"),
            .init(destination: "/"),
        ]

        let sorted = cleanAndSortMounts(mounts)

        // "/tmp/../foo" -> "/foo", "/tmp/./bar/baz" -> "/tmp/bar/baz"
        #expect(sorted.map(\.destination) == ["/", "/foo", "/tmp/bar/baz"])
    }
}

@Suite("AttachedFilesystem runtimeOptions dispatch")
struct AttachedFilesystemTests {

    @Test func virtioblkMountAllocatesBlockDevice() throws {
        let mount = Mount.block(
            format: "ext4",
            source: "/path/to/disk.img",
            destination: "/data"
        )
        let allocator = Character.blockDeviceTagAllocator()
        let attached = try AttachedFilesystem(mount: mount, allocator: allocator)

        #expect(attached.source == "/dev/vda")
        #expect(attached.type == "ext4")
        #expect(attached.destination == "/data")
    }

    @Test func nbdMountAllocatesBlockDevice() throws {
        let mount = Mount.block(
            format: "ext4",
            source: "nbd://localhost:10809",
            destination: "/data"
        )
        let allocator = Character.blockDeviceTagAllocator()
        let attached = try AttachedFilesystem(mount: mount, allocator: allocator)

        #expect(attached.source == "/dev/vda")
        #expect(attached.type == "ext4")
        #expect(attached.destination == "/data")
    }

    @Test func nbdMountWithNonExt4FormatAllocatesBlockDevice() throws {
        let mount = Mount.block(
            format: "xfs",
            source: "nbd://localhost:10809",
            destination: "/data"
        )
        let allocator = Character.blockDeviceTagAllocator()
        let attached = try AttachedFilesystem(mount: mount, allocator: allocator)

        #expect(attached.source == "/dev/vda")
        #expect(attached.type == "xfs")
    }

    @Test func multipleBlockDevicesAllocateSequentially() throws {
        let allocator = Character.blockDeviceTagAllocator()

        let m1 = Mount.block(format: "ext4", source: "/disk1.img", destination: "/a")
        let m2 = Mount.block(format: "ext4", source: "nbd://host:10809", destination: "/b")
        let m3 = Mount.block(format: "ext4", source: "/disk2.img", destination: "/c")

        let a1 = try AttachedFilesystem(mount: m1, allocator: allocator)
        let a2 = try AttachedFilesystem(mount: m2, allocator: allocator)
        let a3 = try AttachedFilesystem(mount: m3, allocator: allocator)

        #expect(a1.source == "/dev/vda")
        #expect(a2.source == "/dev/vdb")
        #expect(a3.source == "/dev/vdc")
    }

    @Test func anyMountUsesSourceDirectly() throws {
        let mount = Mount.any(
            type: "tmpfs",
            source: "tmpfs",
            destination: "/tmp"
        )
        let allocator = Character.blockDeviceTagAllocator()
        let attached = try AttachedFilesystem(mount: mount, allocator: allocator)

        #expect(attached.source == "tmpfs")
    }
}

@Suite("PodVolume and shared mount types")
struct PodVolumeTests {

    @Test func podVolumeNBDSourceCreation() {
        let volume = LinuxPod.PodVolume(
            name: "shared-data",
            source: .nbd(url: URL(string: "nbd://localhost:10809")!),
            format: "ext4"
        )

        #expect(volume.name == "shared-data")
        #expect(volume.format == "ext4")
        if case .nbd(let url, let timeout, let readOnly) = volume.source {
            #expect(url.absoluteString == "nbd://localhost:10809")
            #expect(timeout == nil)
            #expect(readOnly == false)
        } else {
            Issue.record("Expected .nbd source")
        }
    }

    @Test func podVolumeNBDSourceWithOptions() {
        let volume = LinuxPod.PodVolume(
            name: "data",
            source: .nbd(url: URL(string: "nbd://host:10809")!, timeout: 30, readOnly: true),
            format: "xfs"
        )

        if case .nbd(_, let timeout, let readOnly) = volume.source {
            #expect(timeout == 30)
            #expect(readOnly == true)
        } else {
            Issue.record("Expected .nbd source")
        }
    }

    @Test func podVolumeToMountConvertsCorrectly() {
        let volume = LinuxPod.PodVolume(
            name: "my-vol",
            source: .nbd(url: URL(string: "nbd://host:10809/export")!),
            format: "ext4"
        )

        let mount = volume.toMount()

        #expect(mount.source == "nbd://host:10809/export")
        #expect(mount.destination == "/run/volumes/my-vol")
        #expect(mount.type == "ext4")
        #expect(mount.isBlock)
    }

    @Test func podVolumeToMountWithReadOnlySetsOptions() {
        let volume = LinuxPod.PodVolume(
            name: "ro-vol",
            source: .nbd(url: URL(string: "nbd://host:10809")!, readOnly: true),
            format: "ext4"
        )

        let mount = volume.toMount()

        #expect(mount.options.contains("ro"))
        #expect(mount.isBlock)
    }

    @Test func podVolumeToMountWithTimeoutSetsRuntimeOption() {
        let volume = LinuxPod.PodVolume(
            name: "data",
            source: .nbd(url: URL(string: "nbd://host:10809")!, timeout: 60),
            format: "ext4"
        )

        let mount = volume.toMount()

        if case .virtioblk(let opts) = mount.runtimeOptions {
            #expect(opts.contains("vzTimeout=60.0"))
        } else {
            Issue.record("Expected virtioblk runtime options")
        }
    }

    @Test func podVolumeToMountUsesMountType() {
        let volume = LinuxPod.PodVolume(
            name: "data",
            source: .nbd(url: URL(string: "nbd://host:10809")!),
            format: "xfs"
        )

        let mount = volume.toMount()

        #expect(mount.type == "xfs")
    }

    @Test func podVolumeDiskImageSourceCreation() {
        let volume = LinuxPod.PodVolume(
            name: "disk-data",
            source: .diskImage(path: URL(fileURLWithPath: "/tmp/disk.ext4")),
            format: "ext4"
        )

        #expect(volume.name == "disk-data")
        #expect(volume.format == "ext4")
        if case .diskImage(let path, let readOnly) = volume.source {
            #expect(path.path == "/tmp/disk.ext4")
            #expect(readOnly == false)
        } else {
            Issue.record("Expected .diskImage source")
        }
    }

    @Test func podVolumeDiskImageToMountConvertsCorrectly() {
        let volume = LinuxPod.PodVolume(
            name: "my-disk",
            source: .diskImage(path: URL(fileURLWithPath: "/tmp/my-disk.ext4")),
            format: "ext4"
        )

        let mount = volume.toMount()

        // The mount source must be the raw filesystem path, not a file:// URL.
        #expect(mount.source == "/tmp/my-disk.ext4")
        #expect(mount.destination == "/run/volumes/my-disk")
        #expect(mount.type == "ext4")
        #expect(mount.isBlock)
    }

    @Test func podVolumeDiskImageReadOnlySetsOptions() {
        let volume = LinuxPod.PodVolume(
            name: "ro-disk",
            source: .diskImage(path: URL(fileURLWithPath: "/tmp/ro-disk.ext4"), readOnly: true),
            format: "ext4"
        )

        let mount = volume.toMount()

        #expect(mount.options.contains("ro"))
        #expect(mount.isBlock)
    }

    @Test func sharedMountCreation() {
        let mount = Mount.sharedMount(
            name: "shared-data",
            destination: "/data",
            options: ["ro"]
        )

        #expect(mount.source == "shared-data")
        #expect(mount.destination == "/data")
        #expect(mount.options == ["ro"])
        #expect(mount.type == "none")
        if case .shared = mount.runtimeOptions {
            // correct
        } else {
            Issue.record("Expected .shared runtime options")
        }
    }

    @Test func sharedMountDefaultOptions() {
        let mount = Mount.sharedMount(name: "data", destination: "/mnt")

        #expect(mount.options.isEmpty)
    }

    @Test func sharedMountIsNotBlock() {
        let mount = Mount.sharedMount(name: "data", destination: "/mnt")

        #expect(!mount.isBlock)
    }

    @Test func sharedMountDoesNotAllocateBlockDevice() throws {
        let allocator = Character.blockDeviceTagAllocator()

        // Shared mount should not consume a device letter.
        let shared = Mount.sharedMount(name: "vol", destination: "/data")
        let attached = try AttachedFilesystem(mount: shared, allocator: allocator)
        #expect(attached.source == "vol")

        // Next block device should still get vda.
        let block = Mount.block(format: "ext4", source: "/disk.img", destination: "/mnt")
        let blockAttached = try AttachedFilesystem(mount: block, allocator: allocator)
        #expect(blockAttached.source == "/dev/vda")
    }
}
