//
// Copyright 2025 Element Creations Ltd.
// Copyright 2023-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//

@testable import ElementX
import SwiftUI
import Testing
import UniformTypeIdentifiers

final class MediaUploadingPreprocessorTests {
    let maxUploadSize: UInt = 100 * 1024 * 1024
    var appSettings: AppSettings!
    var mediaUploadingPreprocessor: MediaUploadingPreprocessor!
    
    init() {
        AppSettings.resetAllSettings()
        appSettings = AppSettings()
        appSettings.optimizeMediaUploads = false
        ServiceLocator.shared.register(appSettings: appSettings)
        mediaUploadingPreprocessor = MediaUploadingPreprocessor(appSettings: appSettings)
    }
    
    deinit {
        AppSettings.resetAllSettings()
    }
    
    @Test
    func audioFileProcessing() async throws {
        let url = try #require(Bundle(for: Self.self).url(forResource: "test_audio.mp3", withExtension: nil), "Failed retrieving test asset")
        
        guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
              case let .audio(audioURL, audioInfo) = result else {
            Issue.record("Failed processing asset")
            return
        }
        
        // Check that the file name is preserved
        #expect(audioURL.lastPathComponent == "test_audio.mp3")
        
        #expect(audioInfo.mimetype == "audio/mpeg")
        #expect(isEqual(audioInfo.duration ?? 0, 27, within: 100))
        #expect(isEqual(audioInfo.size ?? 0, 194_811, within: 100))
    }
    
    @Test
    func landscapeMovVideoProcessing() async throws {
        let url = try #require(Bundle(for: Self.self).url(forResource: "landscape_test_video.mov", withExtension: nil), "Failed retrieving test asset")
        
        guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
              case let .video(videoURL, thumbnailURL, videoInfo) = result else {
            Issue.record("Failed processing asset")
            return
        }
        
        // Check that the file name is preserved
        #expect(videoURL.lastPathComponent == "landscape_test_video.mp4")
        #expect(videoURL.pathExtension == "mp4", "The file extension should match the container we use.")
        
        // Check that the thumbnail is generated correctly
        let thumbnailData = try Data(contentsOf: thumbnailURL)
        let thumbnail = try #require(UIImage(data: thumbnailData), "Invalid thumbnail")
        
        #expect(thumbnail.size.width <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.width)
        #expect(thumbnail.size.height <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.height)
        
        // Check resulting video info
        #expect(videoInfo.mimetype == "video/mp4")
        #expect(videoInfo.blurhash == "K9F$LJZ9,+8yA9-:yT,@%1")
        #expect(isEqual(videoInfo.size ?? 0, 4_016_620, within: 100))
        #expect(videoInfo.width == 1280)
        #expect(videoInfo.height == 720)
        #expect(isEqual(videoInfo.duration ?? 0, 30, within: 100))
        
        #expect(videoInfo.thumbnailInfo != nil)
        #expect(videoInfo.thumbnailInfo?.mimetype == "image/jpeg")
        #expect(isEqual(videoInfo.thumbnailInfo?.size ?? 0, 183_093, within: 100))
        #expect(videoInfo.thumbnailInfo?.width == 800)
        #expect(videoInfo.thumbnailInfo?.height == 450)
        
        // Repeat with optimised media setting
        appSettings.optimizeMediaUploads = true
        
        guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
              case let .video(optimizedVideoURL, _, optimizedVideoInfo) = optimizedResult else {
            Issue.record("Failed processing asset")
            return
        }
        
        #expect(optimizedVideoURL.pathExtension == "mp4", "The file extension should match the container we use.")
        
        // Check optimised video info
        #expect(optimizedVideoInfo.mimetype == "video/mp4")
        #expect(optimizedVideoInfo.blurhash == "K9F$LJZ9,+8yA9-:yT,@%1")
        #expect(isEqual(optimizedVideoInfo.size ?? 0, 4_016_620, within: 100)) // Note: The video is already 720p so it doesn't change size.
        #expect(optimizedVideoInfo.width == 1280)
        #expect(optimizedVideoInfo.height == 720)
        #expect(isEqual(optimizedVideoInfo.duration ?? 0, 30, within: 100))
    }
    
    @Test
    func portraitMp4VideoProcessing() async throws {
        let url = try #require(Bundle(for: Self.self).url(forResource: "portrait_test_video.mp4", withExtension: nil), "Failed retrieving test asset")
        
        guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
              case let .video(videoURL, thumbnailURL, videoInfo) = result else {
            Issue.record("Failed processing asset")
            return
        }
        
        // Check that the file name is preserved
        #expect(videoURL.lastPathComponent == "portrait_test_video.mp4")
        #expect(videoURL.pathExtension == "mp4", "The file extension should match the container we use.")
        
        // Check that the thumbnail is generated correctly
        let thumbnailData = try Data(contentsOf: thumbnailURL)
        let thumbnail = try #require(UIImage(data: thumbnailData), "Invalid thumbnail")
        
        #expect(thumbnail.size.width <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.width)
        #expect(thumbnail.size.height <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.height)
        
        // Check resulting video info
        #expect(videoInfo.mimetype == "video/mp4")
        #expect(videoInfo.blurhash == "KSB{R8O]MuwQS4oJvcaIt8")
        #expect(isEqual(videoInfo.size ?? 0, 5_824_946, within: 100))
        #expect(videoInfo.width == 1080)
        #expect(videoInfo.height == 1920)
        #expect(isEqual(videoInfo.duration ?? 0, 21, within: 100))
        
        #expect(videoInfo.thumbnailInfo != nil)
        #expect(videoInfo.thumbnailInfo?.mimetype == "image/jpeg")
        #expect(isEqual(videoInfo.thumbnailInfo?.size ?? 0, 40976, within: 100))
        #expect(videoInfo.thumbnailInfo?.width == 337)
        #expect(videoInfo.thumbnailInfo?.height == 600)
        
        // Repeat with optimised media setting
        appSettings.optimizeMediaUploads = true
        
        guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
              case let .video(optimizedVideoURL, _, optimizedVideoInfo) = optimizedResult else {
            Issue.record("Failed processing asset")
            return
        }
        
        #expect(optimizedVideoURL.pathExtension == "mp4", "The file extension should match the container we use.")
        
        // Check optimised video info
        #expect(optimizedVideoInfo.mimetype == "video/mp4")
        #expect(optimizedVideoInfo.blurhash == "KSC5.vO]MuwQS4oJvcaIt8")
        #expect(isEqual(optimizedVideoInfo.size ?? 0, 12_169_117, within: 100)) // Note: This is slightly stupid because it is larger now 🤦‍♂️
        #expect(optimizedVideoInfo.width == 720)
        #expect(optimizedVideoInfo.height == 1280)
        #expect(isEqual(optimizedVideoInfo.duration ?? 0, 30, within: 100))
    }
    
    @Test
    func landscapeImageProcessing() async throws {
        let url = try #require(Bundle(for: Self.self).url(forResource: "landscape_test_image.jpg", withExtension: nil), "Failed retrieving test asset")
        
        guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
              case let .image(convertedImageURL, thumbnailURL, imageInfo) = result else {
            Issue.record("Failed processing asset")
            return
        }
        
        try compare(originalImageAt: url, toConvertedImageAt: convertedImageURL, withThumbnailAt: thumbnailURL)
        
        // Check resulting image info
        #expect(imageInfo.mimetype == "image/jpeg")
        #expect(imageInfo.blurhash == "K%I#.NofkC_4ayayxujsWB")
        #expect(isEqual(imageInfo.size ?? 0, 3_305_795, within: 100))
        #expect(imageInfo.width == 6103)
        #expect(imageInfo.height == 2621)
        
        #expect(imageInfo.thumbnailInfo != nil)
        #expect(imageInfo.thumbnailInfo?.mimetype == "image/jpeg")
        #expect(isEqual(imageInfo.thumbnailInfo?.size ?? 0, 87733, within: 100))
        #expect(imageInfo.thumbnailInfo?.width == 800)
        #expect(imageInfo.thumbnailInfo?.height == 344)
        
        // Repeat with optimised media setting
        appSettings.optimizeMediaUploads = true
        
        guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
              case let .image(optimizedImageURL, thumbnailURL, optimizedImageInfo) = optimizedResult else {
            Issue.record("Failed processing asset")
            return
        }
        
        try compare(originalImageAt: url, toConvertedImageAt: optimizedImageURL, withThumbnailAt: thumbnailURL)
        
        // Check optimised image info
        #expect(optimizedImageInfo.mimetype == "image/jpeg")
        #expect(optimizedImageInfo.blurhash == "K%I#.NofkC_4ayaxxujsWB")
        #expect(isEqual(optimizedImageInfo.size ?? 0, 524_226, within: 100))
        #expect(optimizedImageInfo.width == 2048)
        #expect(optimizedImageInfo.height == 879)
    }
    
    @Test
    func portraitImageProcessing() async throws {
        let url = try #require(Bundle(for: Self.self).url(forResource: "portrait_test_image.jpg", withExtension: nil), "Failed retrieving test asset")
        
        guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
              case let .image(convertedImageURL, thumbnailURL, imageInfo) = result else {
            Issue.record("Failed processing asset")
            return
        }
        
        try compare(originalImageAt: url, toConvertedImageAt: convertedImageURL, withThumbnailAt: thumbnailURL)
        
        // Check resulting image info
        #expect(imageInfo.mimetype == "image/jpeg")
        #expect(imageInfo.blurhash == "KdE|0Ls+RP^-n*RP%OWAV@")
        #expect(isEqual(imageInfo.size ?? 0, 4_414_666, within: 100))
        #expect(imageInfo.width == 3024)
        #expect(imageInfo.height == 4032)
        
        #expect(imageInfo.thumbnailInfo != nil)
        #expect(imageInfo.thumbnailInfo?.mimetype == "image/jpeg")
        #expect(isEqual(imageInfo.thumbnailInfo?.size ?? 0, 258_914, within: 100))
        #expect(imageInfo.thumbnailInfo?.width == 600)
        #expect(imageInfo.thumbnailInfo?.height == 800)
        
        // Repeat with optimised media setting
        appSettings.optimizeMediaUploads = true
        
        guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
              case let .image(optimizedImageURL, thumbnailURL, optimizedImageInfo) = optimizedResult else {
            Issue.record("Failed processing asset")
            return
        }
        
        try compare(originalImageAt: url, toConvertedImageAt: optimizedImageURL, withThumbnailAt: thumbnailURL)
        
        // Check optimised image info
        #expect(optimizedImageInfo.mimetype == "image/jpeg")
        #expect(optimizedImageInfo.blurhash == "KdE|0Ls+RP^-n*RP%OWAV@")
        #expect(isEqual(optimizedImageInfo.size ?? 0, 1_462_937, within: 100))
        #expect(optimizedImageInfo.width == 1536)
        #expect(optimizedImageInfo.height == 2048)
    }
    
    @Test
    func pngImageProcessing() async throws {
        let url = try #require(Bundle(for: Self.self).url(forResource: "test_image.png", withExtension: nil), "Failed retrieving test asset")
        
        guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
              case let .image(convertedImageURL, _, imageInfo) = result else {
            Issue.record("Failed processing asset")
            return
        }
        
        // Make sure the output file matches the image info.
        #expect(mimeType(from: convertedImageURL) == "image/png", "PNGs should always be sent as PNG to preserve the alpha channel.")
        #expect(convertedImageURL.pathExtension == "png", "The file extension should match the MIME type.")
        
        // Check resulting image info
        #expect(imageInfo.mimetype == "image/png")
        #expect(imageInfo.blurhash == "K0TSUA~qfQ~qj[fQfQfQfQ")
        #expect(isEqual(imageInfo.size ?? 0, 4868, within: 100))
        #expect(imageInfo.width == 240)
        #expect(imageInfo.height == 240)
        
        #expect(imageInfo.thumbnailInfo != nil)
        #expect(imageInfo.thumbnailInfo?.mimetype == "image/jpeg")
        #expect(isEqual(imageInfo.thumbnailInfo?.size ?? 0, 1725, within: 100))
        #expect(imageInfo.thumbnailInfo?.width == 240)
        #expect(imageInfo.thumbnailInfo?.height == 240)
        
        // Repeat with optimised media setting
        appSettings.optimizeMediaUploads = true
        
        guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
              case let .image(optimizedImageURL, _, optimizedImageInfo) = optimizedResult else {
            Issue.record("Failed processing asset")
            return
        }
        
        // Make sure the output file matches the image info.
        #expect(mimeType(from: optimizedImageURL) == "image/png", "PNGs should always be sent as PNG to preserve the alpha channel.")
        #expect(optimizedImageURL.pathExtension == "png", "The file extension should match the MIME type.")
        
        // Check optimised image info
        #expect(optimizedImageInfo.mimetype == "image/png")
        #expect(optimizedImageInfo.blurhash == "K0TSUA~qfQ~qj[fQfQfQfQ")
        #expect(isEqual(optimizedImageInfo.size ?? 0, 8199, within: 100))
        // Assert that resizing didn't upscale to the maxPixelSize.
        #expect(optimizedImageInfo.width == 240)
        #expect(optimizedImageInfo.height == 240)
    }
    
    @Test
    func heicImageProcessing() async throws {
        let url = try #require(Bundle(for: Self.self).url(forResource: "test_apple_image.heic", withExtension: nil), "Failed retrieving test asset")
        
        guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
              case let .image(convertedImageURL, thumbnailURL, imageInfo) = result else {
            Issue.record("Failed processing asset")
            return
        }
        
        try compare(originalImageAt: url, toConvertedImageAt: convertedImageURL, withThumbnailAt: thumbnailURL)
        
        // Make sure the output file matches the image info.
        #expect(mimeType(from: convertedImageURL) == "image/heic", "Unoptimised HEICs should always be sent as is.")
        #expect(convertedImageURL.pathExtension == "heic", "The file extension should match the MIME type.")
        
        // Check resulting image info
        #expect(imageInfo.mimetype == "image/heic")
        #expect(imageInfo.blurhash == "KGD]3ns:T00$kWxFXmt6xv")
        #expect(isEqual(imageInfo.size ?? 0, 1_848_525, within: 100))
        #expect(imageInfo.width == 3024)
        #expect(imageInfo.height == 4032)
        
        #expect(imageInfo.thumbnailInfo != nil)
        #expect(imageInfo.thumbnailInfo?.mimetype == "image/jpeg")
        #expect(isEqual(imageInfo.thumbnailInfo?.size ?? 0, 218_108, within: 100))
        #expect(imageInfo.thumbnailInfo?.width == 600)
        #expect(imageInfo.thumbnailInfo?.height == 800)
        
        // Repeat with optimised media setting
        appSettings.optimizeMediaUploads = true
        
        guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
              case let .image(optimizedImageURL, thumbnailURL, optimizedImageInfo) = optimizedResult else {
            Issue.record("Failed processing asset")
            return
        }
        
        try compare(originalImageAt: url, toConvertedImageAt: optimizedImageURL, withThumbnailAt: thumbnailURL)
        
        // Make sure the output file matches the image info.
        #expect(mimeType(from: optimizedImageURL) == "image/jpeg", "Optimised HEICs should always be converted to JPEG for compatibility.")
        #expect(optimizedImageURL.pathExtension == "jpeg", "The file extension should match the MIME type.")
        
        // Check optimised image info
        #expect(optimizedImageInfo.mimetype == "image/jpeg")
        #expect(optimizedImageInfo.blurhash == "KGD]3ns:T00#kWxFb^s:xv")
        #expect(isEqual(optimizedImageInfo.size ?? 0, 1_049_393, within: 100))
        #expect(optimizedImageInfo.width == 1536)
        #expect(optimizedImageInfo.height == 2048)
    }
    
    @Test
    func gifImageProcessing() async throws {
        let url = try #require(Bundle(for: Self.self).url(forResource: "test_animated_image.gif", withExtension: nil), "Failed retrieving test asset")
        let originalSizeValue = try UInt64(FileManager.default.sizeForItem(at: url))
        let originalSize = try #require(originalSizeValue > 0 ? originalSizeValue : nil, "File size must be greater than zero")
        
        guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
              case let .image(convertedImageURL, _, imageInfo) = result else {
            Issue.record("Failed processing asset")
            return
        }
        
        // Make sure the output file matches the image info.
        #expect(mimeType(from: convertedImageURL) == "image/gif", "GIFs should always be sent as GIF to preserve the animation.")
        #expect(convertedImageURL.pathExtension == "gif", "The file extension should match the MIME type.")
        
        // Check resulting image info
        #expect(imageInfo.mimetype == "image/gif")
        #expect(imageInfo.blurhash == "KpRMPTj[_NxuaeRj%MofMx")
        #expect(isEqual(imageInfo.size ?? 0, originalSize, within: 100))
        #expect(imageInfo.width == 331)
        #expect(imageInfo.height == 472)
        
        #expect(imageInfo.thumbnailInfo != nil)
        #expect(imageInfo.thumbnailInfo?.mimetype == "image/jpeg")
        #expect(isEqual(imageInfo.thumbnailInfo?.size ?? 0, 34215, within: 100))
        #expect(imageInfo.thumbnailInfo?.width == 331)
        #expect(imageInfo.thumbnailInfo?.height == 472)
        
        // Repeat with optimised media setting
        appSettings.optimizeMediaUploads = true
        
        guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
              case let .image(optimizedImageURL, _, optimizedImageInfo) = optimizedResult else {
            Issue.record("Failed processing asset")
            return
        }
        
        // Make sure the output file matches the image info.
        #expect(mimeType(from: optimizedImageURL) == "image/gif", "GIFs should always be sent as GIF to preserve the animation.")
        #expect(optimizedImageURL.pathExtension == "gif", "The file extension should match the MIME type.")
        
        // Ensure optimised image is still the same as the original image.
        #expect(optimizedImageInfo.mimetype == "image/gif")
        #expect(optimizedImageInfo.blurhash == "KpRMPTj[_NxuaeRj%MofMx")
        #expect(isEqual(optimizedImageInfo.size ?? 0, originalSize, within: 100))
        #expect(optimizedImageInfo.width == 331)
        #expect(optimizedImageInfo.height == 472)
    }
    
    @Test
    func rotatedImageProcessing() async throws {
        let url = try #require(Bundle(for: Self.self).url(forResource: "test_rotated_image.jpg", withExtension: nil), "Failed retrieving test asset")
        
        guard case let .success(result) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
              case let .image(convertedImageURL, thumbnailURL, imageInfo) = result else {
            Issue.record("Failed processing asset")
            return
        }
        
        try compare(originalImageAt: url, toConvertedImageAt: convertedImageURL, withThumbnailAt: thumbnailURL)
        
        // Check resulting image info
        #expect(imageInfo.mimetype == "image/jpeg")
        #expect(imageInfo.width == 2848)
        #expect(imageInfo.height == 4272)
        
        #expect(imageInfo.thumbnailInfo != nil)
        #expect(imageInfo.thumbnailInfo?.width == 533)
        #expect(imageInfo.thumbnailInfo?.height == 800)
        
        // Repeat with optimised media setting
        appSettings.optimizeMediaUploads = true
        
        guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
              case let .image(optimizedImageURL, thumbnailURL, optimizedImageInfo) = optimizedResult else {
            Issue.record("Failed processing asset")
            return
        }
        
        try compare(originalImageAt: url, toConvertedImageAt: optimizedImageURL, withThumbnailAt: thumbnailURL)
        
        // Check optimised image info
        #expect(optimizedImageInfo.mimetype == "image/jpeg")
        #expect(optimizedImageInfo.width == 1365)
        #expect(optimizedImageInfo.height == 2048)
    }
    
    // MARK: - Private
    
    private func isEqual<N: UnsignedInteger>(_ lhs: N, _ rhs: N, within tolerance: N) -> Bool {
        isEqual(Double(lhs), Double(rhs), within: Double(tolerance))
    }
    
    private func isEqual<N: SignedNumeric & Comparable>(_ lhs: N, _ rhs: N, within tolerance: N) -> Bool {
        abs(lhs - rhs) <= tolerance
    }
    
    private func compare(originalImageAt originalImageURL: URL, toConvertedImageAt convertedImageURL: URL, withThumbnailAt thumbnailURL: URL) throws {
        guard let originalImageData = try? Data(contentsOf: originalImageURL),
              let originalImage = UIImage(data: originalImageData),
              let convertedImageData = try? Data(contentsOf: convertedImageURL),
              let convertedImage = UIImage(data: convertedImageData) else {
            fatalError()
        }
        
        if appSettings.optimizeMediaUploads {
            // Check that new image has been scaled within the requirements for an optimised image
            #expect(convertedImage.size.width <= MediaUploadingPreprocessor.Constants.optimizedMaxPixelSize)
            #expect(convertedImage.size.height <= MediaUploadingPreprocessor.Constants.optimizedMaxPixelSize)
        } else {
            // Check that the file name is preserved
            #expect(originalImageURL.lastPathComponent == convertedImageURL.lastPathComponent)
            // Check that new image is the same size as the original one
            #expect(originalImage.size == convertedImage.size)
        }
        
        // Check that the GPS data has been stripped
        let originalMetadata = try metadata(from: originalImageData)
        #expect(originalMetadata.value(forKeyPath: "\(kCGImagePropertyGPSDictionary)") != nil)
        
        let convertedMetadata = try metadata(from: convertedImageData)
        #expect(convertedMetadata.value(forKeyPath: "\(kCGImagePropertyGPSDictionary)") == nil)
        
        // Check that the thumbnail is generated correctly
        let thumbnailData = try Data(contentsOf: thumbnailURL)
        let thumbnail = try #require(UIImage(data: thumbnailData), "Invalid thumbnail")
        
        if thumbnail.size.width > thumbnail.size.height {
            #expect(thumbnail.size.width <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.width)
            #expect(thumbnail.size.height <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.height)
        } else {
            #expect(thumbnail.size.width <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.height)
            #expect(thumbnail.size.height <= MediaUploadingPreprocessor.Constants.maximumThumbnailSize.width)
        }
        
        let thumbnailMetadata = try metadata(from: thumbnailData)
        #expect(thumbnailMetadata.value(forKeyPath: "\(kCGImagePropertyGPSDictionary)") == nil)
    }
    
    private func metadata(from imageData: Data) throws -> NSDictionary {
        let imageSource = try #require(CGImageSourceCreateWithData(imageData as NSData, nil), "Invalid asset")
        return try #require(CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as NSDictionary?, "Test asset is expected to contain metadata")
    }
    
    private func mimeType(from url: URL) -> String? {
        guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
              let typeIdentifier = CGImageSourceGetType(imageSource),
              let type = UTType(typeIdentifier as String),
              let mimeType = type.preferredMIMEType else {
            Issue.record("Failed to get mimetype from URL.")
            return nil
        }
        return mimeType
    }
}
