//
//  SUFileManagerTest.swift
//  Sparkle
//
//  Created by Mayur Pawashe on 9/26/15.
//  Copyright © 2015 Sparkle Project. All rights reserved.
//

import XCTest

class SUFileManagerTest: XCTestCase
{
    func makeTempFiles(_ testBlock: (SUFileManager, URL, URL, URL, URL, URL, URL) -> Void)
    {
        let fileManager = SUFileManager.default()
        
        let tempDirectoryURL = try! fileManager?.makeTemporaryDirectory(withPreferredName: "Sparkle Unit Test Data", appropriateForDirectoryURL: URL(fileURLWithPath: NSHomeDirectory()))
        
        defer {
            try! fileManager?.removeItem(at: tempDirectoryURL)
        }
        
        let ordinaryFileURL = tempDirectoryURL?.appendingPathComponent("a file written by sparkles unit tests")
        try! "foo".data(using: String.Encoding.utf8)!.write(to: ordinaryFileURL!, options: .atomic)
        
        let directoryURL = tempDirectoryURL?.appendingPathComponent("a directory written by sparkles unit tests")
        try! FileManager.default.createDirectory(at: directoryURL!, withIntermediateDirectories: false, attributes: nil)
        
        let fileInDirectoryURL = directoryURL?.appendingPathComponent("a file inside a directory written by sparkles unit tests")
        try! "bar baz".data(using: String.Encoding.utf8)!.write(to: fileInDirectoryURL!, options: .atomic)
        
        let validSymlinkURL = tempDirectoryURL?.appendingPathComponent("symlink test")
        try! FileManager.default.createSymbolicLink(at: validSymlinkURL!, withDestinationURL: directoryURL!)
        
        let invalidSymlinkURL = tempDirectoryURL?.appendingPathComponent("symlink test 2")
        try! FileManager.default.createSymbolicLink(at: invalidSymlinkURL!, withDestinationURL: (tempDirectoryURL?.appendingPathComponent("does not exist"))!)
        
        testBlock(fileManager!, tempDirectoryURL!, ordinaryFileURL!, directoryURL!, fileInDirectoryURL!, validSymlinkURL!, invalidSymlinkURL!)
    }
    
    func testMoveFiles()
    {
        makeTempFiles() { fileManager, rootURL, ordinaryFileURL, directoryURL, fileInDirectoryURL, validSymlinkURL, invalidSymlinkURL in
            XCTAssertNil(try? fileManager.moveItem(at: ordinaryFileURL, to: directoryURL))
            XCTAssertNil(try? fileManager.moveItem(at: ordinaryFileURL, to: directoryURL.appendingPathComponent("foo").appendingPathComponent("bar")))
            XCTAssertNil(try? fileManager.moveItem(at: rootURL.appendingPathComponent("does not exist"), to: directoryURL))
            
            let newFileURL = (ordinaryFileURL.deletingLastPathComponent().appendingPathComponent("new file"))
            try! fileManager.moveItem(at: ordinaryFileURL, to: newFileURL)
            XCTAssertFalse(fileManager._itemExists(at: ordinaryFileURL))
            XCTAssertTrue(fileManager._itemExists(at: newFileURL))
            
            let newValidSymlinkURL = (ordinaryFileURL.deletingLastPathComponent().appendingPathComponent("new symlink"))
            try! fileManager.moveItem(at: validSymlinkURL, to: newValidSymlinkURL)
            XCTAssertFalse(fileManager._itemExists(at: validSymlinkURL))
            XCTAssertTrue(fileManager._itemExists(at: newValidSymlinkURL))
            XCTAssertTrue(fileManager._itemExists(at: directoryURL))
            
            let newInvalidSymlinkURL = (ordinaryFileURL.deletingLastPathComponent().appendingPathComponent("new invalid symlink"))
            try! fileManager.moveItem(at: invalidSymlinkURL, to: newInvalidSymlinkURL)
            XCTAssertFalse(fileManager._itemExists(at: invalidSymlinkURL))
            XCTAssertTrue(fileManager._itemExists(at: newValidSymlinkURL))
            
            let newDirectoryURL = (ordinaryFileURL.deletingLastPathComponent().appendingPathComponent("new directory"))
            try! fileManager.moveItem(at: directoryURL, to: newDirectoryURL)
            XCTAssertFalse(fileManager._itemExists(at: directoryURL))
            XCTAssertTrue(fileManager._itemExists(at: newDirectoryURL))
            XCTAssertFalse(fileManager._itemExists(at: fileInDirectoryURL))
            XCTAssertTrue(fileManager._itemExists(at: newDirectoryURL.appendingPathComponent(fileInDirectoryURL.lastPathComponent)))
        }
    }
    
    func testMoveFilesToTrash()
    {
        makeTempFiles() { fileManager, rootURL, ordinaryFileURL, directoryURL, fileInDirectoryURL, validSymlinkURL, invalidSymlinkURL in
            XCTAssertNil(try? fileManager.moveItemAtURL(toTrash: rootURL.appendingPathComponent("does not exist")))
            
            let trashURL = try! FileManager.default.url(for: .trashDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
            
            try! fileManager.moveItemAtURL(toTrash: ordinaryFileURL)
            XCTAssertFalse(fileManager._itemExists(at: ordinaryFileURL))
            
            let ordinaryFileTrashURL = trashURL.appendingPathComponent(ordinaryFileURL.lastPathComponent)
            XCTAssertTrue(fileManager._itemExists(at: ordinaryFileTrashURL))
            try! fileManager.removeItem(at: ordinaryFileTrashURL)
            
            let validSymlinkTrashURL = trashURL.appendingPathComponent(validSymlinkURL.lastPathComponent)
            try! fileManager.moveItemAtURL(toTrash: validSymlinkURL)
            XCTAssertTrue(fileManager._itemExists(at: validSymlinkTrashURL))
            XCTAssertTrue(fileManager._itemExists(at: directoryURL))
            try! fileManager.removeItem(at: validSymlinkTrashURL)
            
            try! fileManager.moveItemAtURL(toTrash: directoryURL)
            XCTAssertFalse(fileManager._itemExists(at: directoryURL))
            XCTAssertFalse(fileManager._itemExists(at: fileInDirectoryURL))
            
            let directoryTrashURL = trashURL.appendingPathComponent(directoryURL.lastPathComponent)
            XCTAssertTrue(fileManager._itemExists(at: directoryTrashURL))
            XCTAssertTrue(fileManager._itemExists(at: directoryTrashURL.appendingPathComponent(fileInDirectoryURL.lastPathComponent)))
            
            try! fileManager.removeItem(at: directoryTrashURL)
        }
    }
    
    func testCopyFiles()
    {
        makeTempFiles() { fileManager, rootURL, ordinaryFileURL, directoryURL, fileInDirectoryURL, validSymlinkURL, invalidSymlinkURL in
            XCTAssertNil(try? fileManager.copyItem(at: ordinaryFileURL, to: directoryURL))
            XCTAssertNil(try? fileManager.copyItem(at: ordinaryFileURL, to: directoryURL.appendingPathComponent("foo").appendingPathComponent("bar")))
            XCTAssertNil(try? fileManager.copyItem(at: rootURL.appendingPathComponent("does not exist"), to: directoryURL))
            
            let newFileURL = (ordinaryFileURL.deletingLastPathComponent().appendingPathComponent("new file"))
            try! fileManager.copyItem(at: ordinaryFileURL, to: newFileURL)
            XCTAssertTrue(fileManager._itemExists(at: ordinaryFileURL))
            XCTAssertTrue(fileManager._itemExists(at: newFileURL))
            
            let newSymlinkURL = (ordinaryFileURL.deletingLastPathComponent().appendingPathComponent("new symlink file"))
            try! fileManager.copyItem(at: invalidSymlinkURL, to: newSymlinkURL)
            XCTAssertTrue(fileManager._itemExists(at: newSymlinkURL))
            
            let newDirectoryURL = (ordinaryFileURL.deletingLastPathComponent().appendingPathComponent("new directory"))
            try! fileManager.copyItem(at: directoryURL, to: newDirectoryURL)
            XCTAssertTrue(fileManager._itemExists(at: directoryURL))
            XCTAssertTrue(fileManager._itemExists(at: newDirectoryURL))
            XCTAssertTrue(fileManager._itemExists(at: fileInDirectoryURL))
            XCTAssertTrue(fileManager._itemExists(at: newDirectoryURL.appendingPathComponent(fileInDirectoryURL.lastPathComponent)))
        }
    }

    func testRemoveFiles()
    {
        makeTempFiles() { fileManager, rootURL, ordinaryFileURL, directoryURL, fileInDirectoryURL, validSymlinkURL, invalidSymlinkURL in
            XCTAssertNil(try? fileManager.removeItem(at: rootURL.appendingPathComponent("does not exist")))
            
            try! fileManager.removeItem(at: ordinaryFileURL)
            XCTAssertFalse(fileManager._itemExists(at: ordinaryFileURL))
            
            try! fileManager.removeItem(at: validSymlinkURL)
            XCTAssertFalse(fileManager._itemExists(at: validSymlinkURL))
            XCTAssertTrue(fileManager._itemExists(at: directoryURL))
            
            try! fileManager.removeItem(at: directoryURL)
            XCTAssertFalse(fileManager._itemExists(at: directoryURL))
            XCTAssertFalse(fileManager._itemExists(at: fileInDirectoryURL))
        }
    }
    
    func testReleaseFilesFromQuarantine()
    {
        makeTempFiles() { fileManager, rootURL, ordinaryFileURL, directoryURL, fileInDirectoryURL, validSymlinkURL, invalidSymlinkURL in
            try! fileManager.releaseItemFromQuarantine(atRootURL: ordinaryFileURL)
            try! fileManager.releaseItemFromQuarantine(atRootURL: directoryURL)
            try! fileManager.releaseItemFromQuarantine(atRootURL: validSymlinkURL)
            
            let quarantineData = "does not really matter what is here".cString(using: String.Encoding.utf8)!
            let quarantineDataLength = Int(strlen(quarantineData))
            
            XCTAssertEqual(0, setxattr(ordinaryFileURL.path, SUAppleQuarantineIdentifier, quarantineData, quarantineDataLength, 0, XATTR_CREATE))
            XCTAssertGreaterThan(getxattr(ordinaryFileURL.path, SUAppleQuarantineIdentifier, nil, 0, 0, XATTR_NOFOLLOW), 0)
            
            try! fileManager.releaseItemFromQuarantine(atRootURL: ordinaryFileURL)
            XCTAssertEqual(-1, getxattr(ordinaryFileURL.path, SUAppleQuarantineIdentifier, nil, 0, 0, XATTR_NOFOLLOW))
            
            XCTAssertEqual(0, setxattr(directoryURL.path, SUAppleQuarantineIdentifier, quarantineData, quarantineDataLength, 0, XATTR_CREATE))
            XCTAssertGreaterThan(getxattr(directoryURL.path, SUAppleQuarantineIdentifier, nil, 0, 0, XATTR_NOFOLLOW), 0)
            
            XCTAssertEqual(0, setxattr(fileInDirectoryURL.path, SUAppleQuarantineIdentifier, quarantineData, quarantineDataLength, 0, XATTR_CREATE))
            XCTAssertGreaterThan(getxattr(fileInDirectoryURL.path, SUAppleQuarantineIdentifier, nil, 0, 0, XATTR_NOFOLLOW), 0)
            
            // Extended attributes can't be set on symbolic links currently
            try! fileManager.releaseItemFromQuarantine(atRootURL: validSymlinkURL)
            XCTAssertGreaterThan(getxattr(directoryURL.path, SUAppleQuarantineIdentifier, nil, 0, 0, XATTR_NOFOLLOW), 0)
            XCTAssertEqual(-1, getxattr(validSymlinkURL.path, SUAppleQuarantineIdentifier, nil, 0, 0, XATTR_NOFOLLOW))
            
            try! fileManager.releaseItemFromQuarantine(atRootURL: directoryURL)
            
            XCTAssertEqual(-1, getxattr(directoryURL.path, SUAppleQuarantineIdentifier, nil, 0, 0, XATTR_NOFOLLOW))
            XCTAssertEqual(-1, getxattr(fileInDirectoryURL.path, SUAppleQuarantineIdentifier, nil, 0, 0, XATTR_NOFOLLOW))
        }
    }
    
    func groupIDAtPath(_ path: String) -> gid_t
    {
        let attributes = try! FileManager.default.attributesOfItem(atPath: path)
        let groupID = attributes[FileAttributeKey.groupOwnerAccountID] as! NSNumber
        return groupID.uint32Value
    }
    
    // Only the super user can alter user IDs, so changing user IDs is not tested here
    // Instead we try to change the group ID - we just have to be a member of that group
    func testAlterFilesGroupID()
    {
        makeTempFiles() { fileManager, rootURL, ordinaryFileURL, directoryURL, fileInDirectoryURL, validSymlinkURL, invalidSymlinkURL in
            XCTAssertNil(try? fileManager.changeOwnerAndGroupOfItem(atRootURL: ordinaryFileURL, toMatch: rootURL.appendingPathComponent("does not exist")))
            
            XCTAssertNil(try? fileManager.changeOwnerAndGroupOfItem(atRootURL: rootURL.appendingPathComponent("does not exist"), toMatch: ordinaryFileURL))
            
            let everyoneGroup = getgrnam("everyone")
            let everyoneGroupID = everyoneGroup?.pointee.gr_gid
            
            let staffGroup = getgrnam("staff")
            let staffGroupID = staffGroup?.pointee.gr_gid
            
            XCTAssertNotEqual(staffGroupID, everyoneGroupID)
            
            XCTAssertEqual(staffGroupID, self.groupIDAtPath(ordinaryFileURL.path))
            XCTAssertEqual(staffGroupID, self.groupIDAtPath(directoryURL.path))
            XCTAssertEqual(staffGroupID, self.groupIDAtPath(fileInDirectoryURL.path))
            XCTAssertEqual(staffGroupID, self.groupIDAtPath(validSymlinkURL.path))

            try! fileManager.changeOwnerAndGroupOfItem(atRootURL: fileInDirectoryURL, toMatch: ordinaryFileURL)
            try! fileManager.changeOwnerAndGroupOfItem(atRootURL: ordinaryFileURL, toMatch: ordinaryFileURL)
            try! fileManager.changeOwnerAndGroupOfItem(atRootURL: validSymlinkURL, toMatch: ordinaryFileURL)
            
            XCTAssertEqual(staffGroupID, self.groupIDAtPath(ordinaryFileURL.path))
            XCTAssertEqual(staffGroupID, self.groupIDAtPath(directoryURL.path))
            XCTAssertEqual(staffGroupID, self.groupIDAtPath(validSymlinkURL.path))
            
            XCTAssertEqual(0, chown(ordinaryFileURL.path, getuid(), everyoneGroupID!))
            XCTAssertEqual(everyoneGroupID, self.groupIDAtPath(ordinaryFileURL.path))
            
            try! fileManager.changeOwnerAndGroupOfItem(atRootURL: fileInDirectoryURL, toMatch: ordinaryFileURL)
            XCTAssertEqual(everyoneGroupID, self.groupIDAtPath(fileInDirectoryURL.path))
            
            try! fileManager.changeOwnerAndGroupOfItem(atRootURL: fileInDirectoryURL, toMatch: directoryURL)
            XCTAssertEqual(staffGroupID, self.groupIDAtPath(fileInDirectoryURL.path))
            
            try! fileManager.changeOwnerAndGroupOfItem(atRootURL: validSymlinkURL, toMatch: ordinaryFileURL)
            XCTAssertEqual(everyoneGroupID, self.groupIDAtPath(validSymlinkURL.path))
            
            try! fileManager.changeOwnerAndGroupOfItem(atRootURL: directoryURL, toMatch: ordinaryFileURL)
            XCTAssertEqual(everyoneGroupID, self.groupIDAtPath(directoryURL.path))
            XCTAssertEqual(everyoneGroupID, self.groupIDAtPath(fileInDirectoryURL.path))
        }
    }
    
    func testUpdateFileModificationTime()
    {
        makeTempFiles() { fileManager, rootURL, ordinaryFileURL, directoryURL, fileInDirectoryURL, validSymlinkURL, invalidSymlinkURL in
            XCTAssertNil(try? fileManager.updateModificationAndAccessTimeOfItem(at: rootURL.appendingPathComponent("does not exist")))
            
            let oldOrdinaryFileAttributes = try! FileManager.default.attributesOfItem(atPath: ordinaryFileURL.path)
            let oldDirectoryAttributes = try! FileManager.default.attributesOfItem(atPath: directoryURL.path)
            let oldValidSymlinkAttributes = try! FileManager.default.attributesOfItem(atPath: validSymlinkURL.path)
            
            sleep(1); // wait for clock to advance
            
            try! fileManager.updateModificationAndAccessTimeOfItem(at: ordinaryFileURL)
            try! fileManager.updateModificationAndAccessTimeOfItem(at: directoryURL)
            try! fileManager.updateModificationAndAccessTimeOfItem(at: validSymlinkURL)
            
            let newOrdinaryFileAttributes = try! FileManager.default.attributesOfItem(atPath: ordinaryFileURL.path)
            XCTAssertGreaterThan((newOrdinaryFileAttributes[FileAttributeKey.modificationDate] as! Date).timeIntervalSince(oldOrdinaryFileAttributes[FileAttributeKey.modificationDate] as! Date), 0)
            
            let newDirectoryAttributes = try! FileManager.default.attributesOfItem(atPath: directoryURL.path)
            XCTAssertGreaterThan((newDirectoryAttributes[FileAttributeKey.modificationDate] as! Date).timeIntervalSince(oldDirectoryAttributes[FileAttributeKey.modificationDate] as! Date), 0)
            
            let newSymlinkAttributes = try! FileManager.default.attributesOfItem(atPath: validSymlinkURL.path)
            XCTAssertGreaterThan((newSymlinkAttributes[FileAttributeKey.modificationDate] as! Date).timeIntervalSince(oldValidSymlinkAttributes[FileAttributeKey.modificationDate] as! Date), 0)
        }
    }
    
    func testFileExists()
    {
        makeTempFiles() { fileManager, rootURL, ordinaryFileURL, directoryURL, fileInDirectoryURL, validSymlinkURL, invalidSymlinkURL in
            XCTAssertTrue(fileManager._itemExists(at: ordinaryFileURL))
            XCTAssertTrue(fileManager._itemExists(at: directoryURL))
            XCTAssertFalse(fileManager._itemExists(at: rootURL.appendingPathComponent("does not exist")))
            
            var isOrdinaryFileDirectory: ObjCBool = false
            XCTAssertTrue(fileManager._itemExists(at: ordinaryFileURL, isDirectory: &isOrdinaryFileDirectory) && !isOrdinaryFileDirectory.boolValue)
            
            var isDirectoryADirectory: ObjCBool = false
            XCTAssertTrue(fileManager._itemExists(at: directoryURL, isDirectory: &isDirectoryADirectory) && isDirectoryADirectory.boolValue)
            
            XCTAssertFalse(fileManager._itemExists(at: rootURL.appendingPathComponent("does not exist"), isDirectory: nil))
            
            XCTAssertTrue(fileManager._itemExists(at: validSymlinkURL))
            
            var validSymlinkIsADirectory: ObjCBool = false
            XCTAssertTrue(fileManager._itemExists(at: validSymlinkURL, isDirectory: &validSymlinkIsADirectory) && !validSymlinkIsADirectory.boolValue)
            
            // Symlink should still exist even if it doesn't point to a file that exists
            XCTAssertTrue(fileManager._itemExists(at: invalidSymlinkURL))
            
            var invalidSymlinkIsADirectory: ObjCBool = false
            XCTAssertTrue(fileManager._itemExists(at: invalidSymlinkURL, isDirectory: &invalidSymlinkIsADirectory) && !invalidSymlinkIsADirectory.boolValue)
        }
    }
    
    func testMakeDirectory()
    {
        makeTempFiles() { fileManager, rootURL, ordinaryFileURL, directoryURL, fileInDirectoryURL, validSymlinkURL, invalidSymlinkURL in
            XCTAssertNil(try? fileManager.makeDirectory(at: ordinaryFileURL))
            XCTAssertNil(try? fileManager.makeDirectory(at: directoryURL))
            
            XCTAssertNil(try? fileManager.makeDirectory(at: rootURL.appendingPathComponent("this should").appendingPathComponent("be a failure")))
            
            let newDirectoryURL = rootURL.appendingPathComponent("new test directory")
            XCTAssertFalse(fileManager._itemExists(at: newDirectoryURL))
            try! fileManager.makeDirectory(at: newDirectoryURL)
            
            var isDirectory: ObjCBool = false
            XCTAssertTrue(fileManager._itemExists(at: newDirectoryURL, isDirectory: &isDirectory))
            
            try! fileManager.removeItem(at: directoryURL)
            XCTAssertNil(try? fileManager.makeDirectory(at: validSymlinkURL))
        }
    }
    
    func testAcquireBadAuthorization()
    {
        let fileManager = SUFileManager.default()
        XCTAssertNil(try? fileManager?._acquireAuthorization())
    }
}
