//===----------------------------------------------------------------------===//
// Copyright © 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.
//===----------------------------------------------------------------------===//

#if os(macOS)
import ContainerizationArchive
import ContainerizationEXT4
import ContainerizationExtras
import ContainerizationOCI
import Foundation
import SystemPackage
import Testing

@testable import Containerization

/// Measures header scan overhead vs. full unpack time using real container images
/// pulled from a registry.
///
/// Run with:
///   ENABLE_TIMING_TESTS=1 swift test --filter ImageHeaderScanTimingTest
@Suite
struct ImageHeaderScanTimingTest {
    private static let isEnabled = ProcessInfo.processInfo.environment["ENABLE_TIMING_TESTS"] != nil

    let store: ImageStore
    let dir: URL
    let contentStore: ContentStore

    init() throws {
        let dir = FileManager.default.uniqueTemporaryDirectory(create: true)
        let cs = try LocalContentStore(path: dir)
        let store = try ImageStore(path: dir, contentStore: cs)
        self.dir = dir
        self.store = store
        self.contentStore = cs
    }

    @Test(.enabled(if: ImageHeaderScanTimingTest.isEnabled))
    func measureAlpineAndUbuntu() async throws {
        defer { try? FileManager.default.removeItem(at: dir) }

        let images: [(reference: String, label: String)] = [
            ("ghcr.io/linuxcontainers/alpine:3.20", "Alpine 3.20"),
            ("docker.io/library/ubuntu:24.04", "Ubuntu 24.04"),
        ]

        for image in images {
            print("\n==============================")
            print("Image: \(image.label)")
            print("==============================")

            let img = try await store.pull(reference: image.reference, platform: .current)
            let manifest = try await img.manifest(for: .current)

            for (i, layer) in manifest.layers.enumerated() {
                let content = try await img.getContent(digest: layer.digest)
                let compression = compressionFilter(for: layer.mediaType)
                let compressedSize = try FileManager.default.attributesOfItem(atPath: content.path.path)[.size] as? Int64 ?? 0
                let label = "\(image.label) layer \(i + 1)/\(manifest.layers.count) (\(layer.mediaType), \(formatBytes(compressedSize)) compressed)"
                try await measureOverhead(url: content.path, compression: compression, label: label)
            }
        }
    }

    // MARK: - Helpers

    private func compressionFilter(for mediaType: String) -> ContainerizationArchive.Filter {
        switch mediaType {
        case MediaTypes.imageLayerZstd, MediaTypes.dockerImageLayerZstd:
            return .zstd
        case MediaTypes.imageLayer, MediaTypes.dockerImageLayer:
            return .none
        default:
            return .gzip
        }
    }

    private func measureOverhead(url: URL, compression: ContainerizationArchive.Filter, label: String) async throws {
        let clock = ContinuousClock()

        print("\n--- \(label) ---\n")

        // For zstd, pre-decompress once (matching the production code path in EXT4Unpacker).
        let scanFile: URL
        let scanFilter: ContainerizationArchive.Filter
        var decompressedFile: URL?
        if compression == .zstd {
            var decompressed: URL = url
            let decompressDuration = try clock.measure {
                decompressed = try ArchiveReader.decompressZstd(url)
            }
            scanFile = decompressed
            scanFilter = .none
            decompressedFile = decompressed
            print("  Zstd decompress:      \(decompressDuration)")
        } else {
            scanFile = url
            scanFilter = compression
        }
        defer {
            if let decompressedFile {
                ArchiveReader.cleanUpDecompressedZstd(decompressedFile)
            }
        }

        // 1. Header scan only
        var scannedTotals: (size: Int64, items: Int) = (0, 0)
        let scanDuration = try clock.measure {
            scannedTotals = try EXT4.Formatter.scanArchiveHeaders(
                format: .paxRestricted, filter: scanFilter, file: scanFile)
        }
        print("  Scanned total size:   \(formatBytes(scannedTotals.size)) (\(scannedTotals.items) items)")
        print("  Header scan:          \(scanDuration)")

        // 2. Full unpack without progress
        let tempDir1 = FileManager.default.uniqueTemporaryDirectory()
        let fsPath1 = FilePath(tempDir1.appendingPathComponent("no-progress.ext4.img", isDirectory: false))
        defer { try? FileManager.default.removeItem(at: tempDir1) }

        let unpackOnlyDuration = try await clock.measure {
            let formatter = try EXT4.Formatter(fsPath1)
            try await formatter.unpack(source: url, compression: compression)
            try formatter.close()
        }
        print("  Unpack (no progress): \(unpackOnlyDuration)")

        // 3. Full unpack with progress (includes header scan pass)
        let tempDir2 = FileManager.default.uniqueTemporaryDirectory()
        let fsPath2 = FilePath(tempDir2.appendingPathComponent("with-progress.ext4.img", isDirectory: false))
        defer { try? FileManager.default.removeItem(at: tempDir2) }

        let noopProgress: ProgressHandler = { _ in }
        let withProgressDuration = try await clock.measure {
            let formatter = try EXT4.Formatter(fsPath2)
            try await formatter.unpack(source: url, compression: compression, progress: noopProgress)
            try formatter.close()
        }
        print("  Unpack (w/ progress): \(withProgressDuration)")

        // Summary
        let scanMs = toMs(scanDuration)
        let unpackMs = toMs(unpackOnlyDuration)
        let withProgressMs = toMs(withProgressDuration)
        let overheadMs = withProgressMs - unpackMs
        let overheadPct = unpackMs > 0 ? (overheadMs / unpackMs) * 100 : 0

        print("\n  Summary:")
        print("    Header scan alone:  \(String(format: "%.1f", scanMs)) ms")
        print("    Unpack only:        \(String(format: "%.1f", unpackMs)) ms")
        print("    Unpack + progress:  \(String(format: "%.1f", withProgressMs)) ms")
        print("    Overhead:           \(String(format: "%.1f", overheadMs)) ms (\(String(format: "%.1f", overheadPct))%)")
    }

    private func toMs(_ d: Duration) -> Double {
        let c = d.components
        return Double(c.seconds) * 1000.0 + Double(c.attoseconds) / 1e15
    }

    private func formatBytes(_ bytes: Int64) -> String {
        let mb = Double(bytes) / 1_048_576.0
        return "\(String(format: "%.1f", mb)) MB"
    }
}
#endif
