//===----------------------------------------------------------------------===//
// 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 ContainerizationExtras
import Foundation
import SystemPackage
import Testing

@testable import ContainerPersistence

struct ConfigurationLoaderTests {
    private static func writeToml(_ contents: String, to path: FilePath) throws {
        try contents.write(
            to: URL(filePath: path.string),
            atomically: true,
            encoding: .utf8
        )
    }

    /// Lenient plugin config used by most `loadForPlugin` tests: missing `cpu`
    /// decodes to the default of 99, so tests can assert "default" vs "loaded".
    private struct TestPluginConfig: LoadablePluginConfiguration {
        static let pluginId = "foo"
        var cpu: Int
        init() { self.cpu = 99 }
        init(from decoder: any Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.cpu = try container.decodeIfPresent(Int.self, forKey: .cpu) ?? 99
        }
    }

    /// A structurally different plugin config — strict decode of `memory`.
    /// Used by:
    /// - `loadForPluginIsolatesFromHostileSibling`: paired with a sibling
    ///   `[plugin.*]` section that also defines `memory`, so a scoping leak
    ///   would surface as a wrong value rather than a thrown error.
    /// - `loadForPluginMalformedSectionThrows`: exercises strict decode against
    ///   a malformed target section.
    private struct TestPluginConfig2: LoadablePluginConfiguration {
        static let pluginId = "mem"
        var memory: String
        init() { self.memory = "1g" }
        init(from decoder: any Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.memory = try container.decode(String.self, forKey: .memory)
        }
    }

    /// Plugin config with an empty `pluginId` to exercise the empty-id guard.
    private struct EmptyIdPluginConfig: LoadablePluginConfiguration {
        static let pluginId = ""
        init() {}
    }

    @Test func layeredFilesPerKeyPrecedence() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let appRoot = tempDir.appending("appRoot.toml")
            let installRoot = tempDir.appending("installRoot.toml")
            try Self.writeToml("[build]\nrosetta = false\n", to: appRoot)
            try Self.writeToml("[registry]\ndomain = \"foo.bar\"\n", to: installRoot)

            let config: ContainerSystemConfig = try await ConfigurationLoader.load(
                configurationFiles: [appRoot, installRoot]
            )
            #expect(config.build.rosetta == false)
            #expect(config.registry.domain == "foo.bar")
        }
    }

    @Test func defaultsWithNoFile() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let path = tempDir.appending("nonexistent.toml")
            let config: ContainerSystemConfig = try await ConfigurationLoader.load(configurationFiles: [path])
            #expect(config.build.rosetta == true)
            #expect(config.build.cpus == 2)
            #expect(config.build.memory == BuildConfig.defaultMemory)
            #expect(config.container.cpus == 4)
            #expect(config.container.memory == ContainerConfig.defaultMemory)
            #expect(config.dns.domain == nil)
            #expect(!config.build.image.isEmpty)
            #expect(!config.vminit.image.isEmpty)
            #expect(!config.kernel.binaryPath.isEmpty)
            #expect(!config.kernel.url.absoluteString.isEmpty)
            #expect(config.kernel.digest == KernelConfig.defaultDigest)
            #expect(config.network.subnet == nil)
            #expect(config.network.subnetv6 == nil)
            #expect(config.registry.domain == "docker.io")
        }
    }

    @Test func tomlOverrideAllKeys() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let toml = """
                [build]
                rosetta = false
                cpus = 8
                memory = "4096MB"
                image = "custom-builder:latest"

                [container]
                cpus = 16
                memory = "8g"

                [dns]
                domain = "custom"

                [kernel]
                binaryPath = "custom/path"
                url = "https://example.com/kernel.tar"
                digest = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"

                [network]
                subnet = "10.0.0.1/16"
                subnetv6 = "fd01::/48"

                [registry]
                domain = "ghcr.io"

                [vminit]
                image = "custom-init:latest"
                """
            let tmpFile = tempDir.appending("test.toml")
            try Self.writeToml(toml, to: tmpFile)

            let config: ContainerSystemConfig = try await ConfigurationLoader.load(configurationFiles: [tmpFile])
            #expect(config.build.rosetta == false)
            #expect(config.build.cpus == 8)
            let expectedBuildMemory = try MemorySize("4096MB")
            #expect(config.build.memory == expectedBuildMemory)
            #expect(config.container.cpus == 16)
            let expectedContainerMemory = try MemorySize("8g")
            #expect(config.container.memory == expectedContainerMemory)
            #expect(config.dns.domain == "custom")
            #expect(config.build.image == "custom-builder:latest")
            #expect(config.vminit.image == "custom-init:latest")
            #expect(config.kernel.binaryPath == "custom/path")
            #expect(config.kernel.url.absoluteString == "https://example.com/kernel.tar")
            #expect(config.kernel.digest == "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
            let expectedSubnet = try CIDRv4("10.0.0.1/16")
            let expectedSubnetV6 = try CIDRv6("fd01::/48")
            #expect(config.network.subnet == expectedSubnet)
            #expect(config.network.subnetv6 == expectedSubnetV6)
            #expect(config.registry.domain == "ghcr.io")
        }
    }

    @Test func partialToml() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let toml = """
                [build]
                cpus = 16
                """
            let tmpFile = tempDir.appending("test.toml")
            try Self.writeToml(toml, to: tmpFile)

            let config: ContainerSystemConfig = try await ConfigurationLoader.load(configurationFiles: [tmpFile])
            #expect(config.build.cpus == 16)
            #expect(config.build.rosetta == true)
            #expect(config.build.memory == BuildConfig.defaultMemory)
            #expect(config.container.cpus == 4)
            #expect(config.container.memory == ContainerConfig.defaultMemory)
        }
    }

    @Test func customKernelURLWithoutDigestThrows() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let toml = """
                [kernel]
                url = "https://example.com/custom-kernel.tar"
                """
            let tmpFile = tempDir.appending("test.toml")
            try Self.writeToml(toml, to: tmpFile)

            await #expect(throws: (any Error).self) {
                let _: ContainerSystemConfig = try await ConfigurationLoader.load(configurationFiles: [tmpFile])
            }
        }
    }

    @Test func layeredCustomKernelURLCanUseDigestFromLowerLayer() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let userFile = tempDir.appending("user.toml")
            let systemFile = tempDir.appending("system.toml")

            try Self.writeToml(
                """
                [kernel]
                url = "https://example.com/custom-kernel.tar"
                """, to: userFile)
            try Self.writeToml(
                """
                [kernel]
                digest = "\(KernelConfig.defaultDigest)"
                """, to: systemFile)

            let config: ContainerSystemConfig = try await ConfigurationLoader.load(
                configurationFiles: [userFile, systemFile])
            #expect(config.kernel.url.absoluteString == "https://example.com/custom-kernel.tar")
            #expect(config.kernel.digest == KernelConfig.defaultDigest)
        }
    }

    @Test func customKernelURLWithDigestCanBeConstructed() {
        let digest = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
        let config = KernelConfig(url: URL(string: "https://example.com/custom-kernel.tar")!, digest: digest)
        #expect(config.url.absoluteString == "https://example.com/custom-kernel.tar")
        #expect(config.digest == digest)
    }

    @Test func unknownKeysIgnored() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let toml = """
                [build]
                cpus = 4
                unknownBuildKey = "ignored"

                [unknownSection]
                foo = "bar"
                """
            let tmpFile = tempDir.appending("test.toml")
            try Self.writeToml(toml, to: tmpFile)

            let config: ContainerSystemConfig = try await ConfigurationLoader.load(configurationFiles: [tmpFile])
            #expect(config.build.cpus == 4)
            #expect(config.build.rosetta == true)
        }
    }

    @Test func invalidTomlThrows() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let tmpFile = tempDir.appending("test-invalid.toml")
            try Self.writeToml("this is [not valid toml", to: tmpFile)
            await #expect(throws: (any Error).self) {
                let _: ContainerSystemConfig = try await ConfigurationLoader.load(configurationFiles: [tmpFile])
            }
        }
    }

    @Test func emptyTomlDecodesToDefaults() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let tmpFile = tempDir.appending("test.toml")
            try Self.writeToml("", to: tmpFile)

            let config: ContainerSystemConfig = try await ConfigurationLoader.load(configurationFiles: [tmpFile])
            #expect(config.build.rosetta == true)
            #expect(config.build.cpus == 2)
            #expect(config.container.cpus == 4)
            #expect(config.registry.domain == "docker.io")
        }
    }

    @Test func loadFollowsSymlinkedSourceFile() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let realFile = tempDir.appending("real-config.toml")
            try Self.writeToml("[build]\ncpus = 8\n", to: realFile)

            let symlinkPath = tempDir.appending("config.toml")
            try FileManager.default.createSymbolicLink(
                atPath: symlinkPath.string, withDestinationPath: realFile.string)

            let config: ContainerSystemConfig = try await ConfigurationLoader.load(
                configurationFiles: [symlinkPath]
            )
            #expect(config.build.cpus == 8)
        }
    }

    @Test func copyConfigToAppRoot() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let source = tempDir.appending("config.toml")
            try Self.writeToml("[build]\ncpus = 8", to: source)

            let destBase = tempDir.appending("dest")
            try ConfigurationLoader.copyConfigurationToReadOnly(from: source, to: destBase)

            let destFile = destBase.appending("config").appending("config.toml")
            let copied = try String(contentsOf: URL(filePath: destFile.string), encoding: .utf8)
            #expect(copied.contains("cpus = 8"))

            let attrs = try FileManager.default.attributesOfItem(atPath: destFile.string)
            let perms = try #require(attrs[.posixPermissions] as? Int)
            #expect(perms == 0o444)
        }
    }

    @Test func copyConfigOverwritesExistingReadOnlyDestination() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let source = tempDir.appending("config.toml")
            let destBase = tempDir.appending("dest")

            try Self.writeToml("[build]\ncpus = 8", to: source)
            try ConfigurationLoader.copyConfigurationToReadOnly(from: source, to: destBase)

            try Self.writeToml("[build]\ncpus = 16", to: source)
            try ConfigurationLoader.copyConfigurationToReadOnly(from: source, to: destBase)

            let destFile = destBase.appending("config").appending("config.toml")
            let copied = try String(contentsOf: URL(filePath: destFile.string), encoding: .utf8)
            #expect(copied.contains("cpus = 16"))

            let attrs = try FileManager.default.attributesOfItem(atPath: destFile.string)
            let perms = try #require(attrs[.posixPermissions] as? Int)
            #expect(perms == 0o444)
        }
    }

    @Test func copyConfigNoOpsWhenSourceMissing() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let source = tempDir.appending("nonexistent.toml")
            let destBase = tempDir.appending("dest")
            try ConfigurationLoader.copyConfigurationToReadOnly(from: source, to: destBase)
            let destFile = destBase.appending("config").appending("config.toml")
            #expect(!FileManager.default.fileExists(atPath: destFile.string))
        }
    }

    @Test func copyConfigDoesNotFollowDestinationSymlink() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let source = tempDir.appending("config.toml")
            try Self.writeToml("[build]\ncpus = 8", to: source)

            let outsideDir = tempDir.appending("outside")
            try FileManager.default.createDirectory(
                atPath: outsideDir.string, withIntermediateDirectories: true)
            let outsideFile = outsideDir.appending("do-not-touch")
            try Self.writeToml("untouched", to: outsideFile)

            let destBase = tempDir.appending("dest")
            let destFile = destBase.appending("config").appending("config.toml")
            try FileManager.default.createDirectory(
                atPath: destFile.removingLastComponent().string,
                withIntermediateDirectories: true)
            try FileManager.default.createSymbolicLink(
                atPath: destFile.string, withDestinationPath: outsideDir.string)

            try ConfigurationLoader.copyConfigurationToReadOnly(from: source, to: destBase)

            var isDirectory: ObjCBool = false
            let exists = FileManager.default.fileExists(atPath: destFile.string, isDirectory: &isDirectory)
            #expect(exists)
            #expect(!isDirectory.boolValue)

            let attrs = try FileManager.default.attributesOfItem(atPath: destFile.string)
            #expect(attrs[.type] as? FileAttributeType != .typeSymbolicLink)
            let perms = try #require(attrs[.posixPermissions] as? Int)
            #expect(perms == 0o444)

            let copied = try String(contentsOf: URL(filePath: destFile.string), encoding: .utf8)
            #expect(copied.contains("cpus = 8"))

            let outsideContents = try String(contentsOf: URL(filePath: outsideFile.string), encoding: .utf8)
            #expect(outsideContents == "untouched")
        }
    }

    @Test func loadForPluginDecodesQualifiedSection() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let toml = """
                [build]
                cpus = 8

                [plugin.foo]
                cpu = 4

                [plugin.other]
                unrelated = "value that would not decode as TestPluginConfig"
                """
            let tmpFile = tempDir.appending("test.toml")
            try Self.writeToml(toml, to: tmpFile)

            let foo: TestPluginConfig = try await ConfigurationLoader.loadForPlugin(
                configurationFiles: [tmpFile])
            #expect(foo.cpu == 4)
        }
    }

    @Test func loadForPluginMissingSectionReturnsDefault() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let toml = """
                [build]
                cpus = 8
                """
            let tmpFile = tempDir.appending("test.toml")
            try Self.writeToml(toml, to: tmpFile)

            let foo: TestPluginConfig = try await ConfigurationLoader.loadForPlugin(
                configurationFiles: [tmpFile])
            #expect(foo.cpu == 99)
        }
    }

    @Test func loadForPluginSubsectionMissingReturnsDefault() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let toml = """
                [plugin.other]
                cpu = 4
                """
            let tmpFile = tempDir.appending("test.toml")
            try Self.writeToml(toml, to: tmpFile)

            let foo: TestPluginConfig = try await ConfigurationLoader.loadForPlugin(
                configurationFiles: [tmpFile])
            #expect(foo.cpu == 99)
        }
    }

    @Test func loadForPluginFileMissingReturnsDefault() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let missing = tempDir.appending("nonexistent.toml")
            let foo: TestPluginConfig = try await ConfigurationLoader.loadForPlugin(
                configurationFiles: [missing])
            #expect(foo.cpu == 99)
        }
    }

    @Test func loadForPluginIsolatesFromHostileSibling() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let toml = """
                [plugin.mem]
                memory = "2g"

                [plugin.other]
                memory = "999g"
                """
            let tmpFile = tempDir.appending("test.toml")
            try Self.writeToml(toml, to: tmpFile)

            let mem: TestPluginConfig2 = try await ConfigurationLoader.loadForPlugin(
                configurationFiles: [tmpFile])
            #expect(mem.memory == "2g")
        }
    }

    @Test func loadForPluginMalformedSectionThrows() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let toml = """
                [plugin.mem]
                memory = 42
                """
            let tmpFile = tempDir.appending("test.toml")
            try Self.writeToml(toml, to: tmpFile)

            await #expect(throws: (any Error).self) {
                let _: TestPluginConfig2 = try await ConfigurationLoader.loadForPlugin(
                    configurationFiles: [tmpFile])
            }
        }
    }

    @Test func loadForPluginEmptyIdThrows() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let tmpFile = tempDir.appending("test.toml")
            try Self.writeToml("[plugin.foo]\ncpu = 4", to: tmpFile)

            await #expect(throws: (any Error).self) {
                let _: EmptyIdPluginConfig = try await ConfigurationLoader.loadForPlugin(
                    configurationFiles: [tmpFile])
            }
        }
    }

    @Test func layeredPrecedence() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let userFile = tempDir.appending("user.toml")
            let systemFile = tempDir.appending("system.toml")

            try Self.writeToml(
                """
                [build]
                cpus = 8
                """, to: userFile)

            try Self.writeToml(
                """
                [build]
                cpus = 4
                memory = "4096MB"
                """, to: systemFile)

            let config: ContainerSystemConfig = try await ConfigurationLoader.load(
                configurationFiles: [userFile, systemFile]
            )
            #expect(config.build.cpus == 8)
            let expectedMemory = try MemorySize("4096MB")
            #expect(config.build.memory == expectedMemory)
        }
    }

    @Test func allFilesMissingReturnsDefaults() async throws {
        let config: ContainerSystemConfig = try await ConfigurationLoader.load(
            configurationFiles: [FilePath("/nonexistent/a.toml"), FilePath("/nonexistent/b.toml")]
        )
        #expect(config.build.rosetta == true)
        #expect(config.build.cpus == 2)
    }

    @Test func partialOverlapMergesKeys() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let userFile = tempDir.appending("user.toml")
            let systemFile = tempDir.appending("system.toml")

            try Self.writeToml(
                """
                [dns]
                domain = "user.local"
                """, to: userFile)

            try Self.writeToml(
                """
                [build]
                cpus = 16
                """, to: systemFile)

            let config: ContainerSystemConfig = try await ConfigurationLoader.load(
                configurationFiles: [userFile, systemFile]
            )
            #expect(config.dns.domain == "user.local")
            #expect(config.build.cpus == 16)
        }
    }

    @Test func pluginLayering() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let userFile = tempDir.appending("user.toml")
            let systemFile = tempDir.appending("system.toml")

            try Self.writeToml(
                """
                [plugin.foo]
                cpu = 8
                """, to: userFile)

            try Self.writeToml(
                """
                [plugin.foo]
                cpu = 2
                """, to: systemFile)

            let config: TestPluginConfig = try await ConfigurationLoader.loadForPlugin(
                configurationFiles: [userFile, systemFile]
            )
            #expect(config.cpu == 8)
        }
    }

    @Test func loadErrorIdentifiesMalformedLayerFile() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let userFile = tempDir.appending("user.toml")
            let systemFile = tempDir.appending("system.toml")

            try Self.writeToml("[build]\ncpus = 8", to: userFile)
            try Self.writeToml("this is [not valid toml", to: systemFile)

            let error = try #require(
                await #expect(throws: (any Error).self) {
                    let _: ContainerSystemConfig = try await ConfigurationLoader.load(
                        configurationFiles: [userFile, systemFile])
                }
            )
            #expect(String(describing: error).contains(systemFile.string))
        }
    }

    @Test func loadForPluginErrorIdentifiesMalformedLayerFile() async throws {
        try await TemporaryStorage.withTempDir { tempDir in
            let userFile = tempDir.appending("user.toml")
            let systemFile = tempDir.appending("system.toml")

            try Self.writeToml("this is [not valid toml", to: userFile)
            try Self.writeToml("[plugin.foo]\ncpu = 2", to: systemFile)

            let error = try #require(
                await #expect(throws: (any Error).self) {
                    let _: TestPluginConfig = try await ConfigurationLoader.loadForPlugin(
                        configurationFiles: [userFile, systemFile])
                }
            )
            #expect(String(describing: error).contains(userFile.string))
        }
    }
}
