//
//  UIViewAnimationTests.swift
//  UIKitTests
//
//  Created by Michael Knoch on 11.08.17.
//  Copyright © 2017 flowkey. All rights reserved.
//

import XCTest
@testable import UIKit

class UIViewAnimationTests: XCTestCase {
    var view = UIView()

    override func tearDown() {
        // reset animation state
        UIView.layersWithAnimations = Set<UIKit.CALayer>()
    }

    override func setUp() {
        view = UIView()
        view.layer.hasBeenRenderedInThisPartOfOverallLayerHierarchy = true
    }

    func testCanAnimateFrame() {
        let frameToStartFrom = CGRect(x: 10, y: 10, width: 10, height: 10)
        let expectedFrame = CGRect(x: 20, y: 20, width: 20, height: 20)

        view.frame = frameToStartFrom

        UIView.animate(withDuration: 5.0, delay: 0, options: [.curveLinear], animations: {
            view.frame = expectedFrame
        })

        XCTAssertEqual(view.frame, expectedFrame)

        UIView.animateIfNeeded(at: Timer(startingAt: 2500))

        guard let presentation = view.layer._presentation else {
            return XCTFail("presentation must be defined")
        }

        XCTAssertEqual(
            presentation.frame.rounded(accuracy: 0.01),
            CGRect(x: 15, y: 15, width: 15, height: 15)
        )
    }

    func testCanAnimateOpacity() {
        UIView.animate(withDuration: 5, delay: 0, options: [.curveLinear], animations: {
            view.alpha = 0.2
        })

        XCTAssertEqual(view.alpha, 0.2, accuracy: 0.001)
        XCTAssertEqual(view.layer.animations.count, 1)

        UIView.animateIfNeeded(at: Timer(startingAt: 2500))
        if let presentation = view.layer._presentation {
            XCTAssertEqual(presentation.opacity, 0.6, accuracy: 0.01)
        } else {
            XCTFail("presentation must be defined")
        }
    }

    func testCanAnimateBounds() {
        let boundsToStartFrom = CGRect(x: 10, y: 10, width: 10, height: 10)
        let expectedBounds = CGRect(x: 20, y: 20, width: 20, height: 20)

        view.bounds = boundsToStartFrom

        UIView.animate(withDuration: 5, delay: 0, options: [.curveLinear], animations: {
            view.bounds = expectedBounds
        })
        XCTAssertEqual(view.bounds, expectedBounds)

        // animating bounds consists of bounds.origin and frame.size animation
        // because mutating frame mutates bounds and vice versa
        XCTAssertEqual(view.layer.animations.count, 1)

        UIView.animateIfNeeded(at: Timer(startingAt: 2500))

        if let presentation = view.layer._presentation {
            XCTAssertEqual(
                presentation.bounds.rounded(accuracy: 0.01),
                CGRect(x: 15, y: 15, width: 15, height: 15)
            )
        } else {
            XCTFail("presentation must be defined")
        }
    }

    func testDelay() {


        UIView.animate(withDuration: 2, delay: 2, options: [.curveLinear], animations: {
            view.alpha = 0
        })

        UIView.animateIfNeeded(at: Timer(startingAt: 3000))
        if let presentation = view.layer._presentation {
            XCTAssertEqual(Double(presentation.opacity), 0.5, accuracy: 0.01)
        } else {
            XCTFail("presentation must be defined")
        }
    }

    func testCompletion() {
        let view = UIView()
        var animationDidFinish = false

        UIView.animate(withDuration: 5, delay: 0, options: [], animations: {
            view.frame = CGRect(x: 20, y: 20, width: 20, height: 20)
            view.alpha = 0.3
            view.bounds.origin.x += 10
        }, completion: {
            animationDidFinish = $0
        })

        UIView.animateIfNeeded(at: Timer(startingAt: 5000))
        XCTAssertTrue(animationDidFinish)
    }

    func testCompletionWhenCancelingAnimations() {
        var firstAnimationDidFinish: Bool?
        var secondAnimationDidFinish: Bool?

        UIView.animate(withDuration: 1, delay: 0, options: [], animations: {
            view.alpha = 0.3
        }, completion: {
            firstAnimationDidFinish = $0
        })

        UIView.animate(withDuration: 1, delay: 0.2, options: [], animations: {
            view.alpha = 0
        }, completion: {
            secondAnimationDidFinish = $0
        })

        UIView.animateIfNeeded(at: Timer(startingAt: 200))
        UIView.animateIfNeeded(at: Timer(startingAt: 5000))

        if
            let firstAnimationDidFinish = firstAnimationDidFinish,
            let secondAnimationDidFinish = secondAnimationDidFinish {
            XCTAssertFalse(firstAnimationDidFinish)
            XCTAssertTrue(secondAnimationDidFinish)
        } else {
            XCTFail("completion callback never called")
        }
    }

    func testCompletionWhenQueingAnimationsOfSameType() {
        var firstAnimationDidFinish: Bool?
        var secondAnimationDidFinish: Bool?

        UIView.animate(withDuration: 1, delay: 0, options: [], animations: {
            view.alpha = 0.3
        }, completion: {
            firstAnimationDidFinish = $0
        })

        UIView.animate(withDuration: 1, delay: 2, options: [], animations: {
            view.alpha = 0
        }, completion: {
            secondAnimationDidFinish = $0
        })

        UIView.animateIfNeeded(at: Timer(startingAt: 5000))

        if
            let firstAnimationDidFinish = firstAnimationDidFinish,
            let secondAnimationDidFinish = secondAnimationDidFinish {
            XCTAssertFalse(firstAnimationDidFinish)
            XCTAssertTrue(secondAnimationDidFinish)
        } else {
            XCTFail("completion callback never called")
        }
    }

    func testPresentationLayerIsSet() {
        let expectedFrame = CGRect(x: 20, y: 20, width: 20, height: 20)

        XCTAssertNil(view.layer._presentation)

        UIView.animate(withDuration: 5, delay: 0, options: [], animations: {
            view.frame = expectedFrame
            view.alpha = 0.1
        })

        UIView.animateIfNeeded(at: Timer(startingAt: 2500))
        XCTAssertNotNil(view.layer._presentation)
    }

    func testPresentationIsRemovedWhenAnimationCompletes() {
        let view = UIView()

        let expectedFrame = CGRect(x: 20, y: 20, width: 20, height: 20)

        UIView.animate(withDuration: 5, delay: 0, options: [], animations: {
            view.frame = expectedFrame
            view.alpha = 0.1
        })

        UIView.animateIfNeeded(at: Timer(startingAt: 6000))
        XCTAssertNil(view.layer._presentation)
    }

    func testLayersWithAnimations() {
        let firstView = UIView()
        firstView.layer.hasBeenRenderedInThisPartOfOverallLayerHierarchy = true

        let secondView = UIView()
        secondView.layer.hasBeenRenderedInThisPartOfOverallLayerHierarchy = true

        let thirdView = UIView()
        thirdView.layer.hasBeenRenderedInThisPartOfOverallLayerHierarchy = true

        UIView.animate(withDuration: 10, delay: 0, options: [], animations: {
            firstView.frame.origin.x += 10
            firstView.alpha = 0.1
        })

        XCTAssertEqual(UIView.layersWithAnimations.count, 1)

        UIView.animate(withDuration: 15, delay: 0, options: [], animations: {
            secondView.alpha = 0.5
            thirdView.alpha = 0.3
        })

        XCTAssertEqual(UIView.layersWithAnimations.count, 3)

        // finish first animation
        UIView.animateIfNeeded(at: Timer(startingAt: 10000))
        XCTAssertEqual(UIView.layersWithAnimations.count, 2)

        // finish second animation
        UIView.animateIfNeeded(at: Timer(startingAt: 15000))
        XCTAssertEqual(UIView.layersWithAnimations.count, 0)
    }

    func testBeginFromCurrentState() {
        UIView.animate(withDuration: 10, delay: 0, options: [.curveLinear], animations: {
            view.bounds.origin.x = 10
        })

        UIView.animateIfNeeded(at: Timer(startingAt: 5000))
        UIView.animate(withDuration: 10, delay: 5, options: [.beginFromCurrentState], animations: {
            view.bounds.origin.x = 20
        })

        guard let fromValue = view.layer.animations["bounds"]?.fromValue as? CGRect else {
            return XCTFail("fromValue must not be nil!")
        }

        XCTAssertEqual(fromValue.origin.x, CGFloat(5.0), accuracy: 0.01)
    }

    func testModifyLayerWhileAnimationIsInFlight() {
        UIView.animate(withDuration: 10, delay: 0, options: [.curveLinear], animations: {
            view.frame.origin.x = 200
        })

        view.alpha = 0
        UIView.animateIfNeeded(at: Timer(startingAt: 5000))

        if let presentation = view.layer._presentation {
            XCTAssertEqual(presentation.opacity, 0)
            XCTAssertEqual(presentation.frame.origin.x, 100, accuracy: 0.01)
        } else {
            XCTFail("presentation must be defined")
        }

    }

    func testModifyPropertyWhichIsCurrentlyAnimating() {
        UIView.animate(withDuration: 10, delay: 0, options: [.curveLinear], animations: {
            view.frame.origin.x = 200
        })

        view.frame.origin.x = 0
        UIView.animateIfNeeded(at: Timer(startingAt: 5000))

        if let presentation = view.layer._presentation {
            XCTAssertEqual(presentation.frame.origin.x, 0)
        } else {
            XCTFail("presentation must be defined")
        }
        XCTAssertEqual(view.frame.origin.x, 0)
    }

    func testAllowUserInteraction() {
        UIView.animate(withDuration: 10, delay: 0, options: [.allowUserInteraction], animations: {
            view.frame.origin.x = 200
        })

        XCTAssertTrue((view.layer.animations["position"]?.animationGroup?
            .options.contains(.allowUserInteraction) ?? false))
        XCTAssertTrue(view.anyCurrentlyRunningAnimationsAllowUserInteraction)
    }

    func testCreateAnimationsOnlyWhenPropertiesDiffer() {
        view.frame.origin.x = 100
        var firstAnimationDidFinish = false

        UIView.animate(withDuration: 10, delay: 0, options: [], animations: {
            view.frame.origin.x = 100
        }) { finished in
            if firstAnimationDidFinish { XCTFail("completion should be called only once")}
            firstAnimationDidFinish = finished
        }

        UIView.animateIfNeeded(at: Timer(startingAt: 10000))
        XCTAssertTrue(view.layer.animations.isEmpty)
        // nevertheless completion should be called
        XCTAssertTrue(firstAnimationDidFinish)
    }

    func testCompletionIsCalledOnlyOnce() {
        var completionCounter = 0

        UIView.animate(withDuration: 5, delay: 0, options: [], animations: {
            view.frame.origin.x = 100
        }) { _ in
            completionCounter += 1
        }

        UIView.animateIfNeeded(at: Timer(startingAt: 10000))
        XCTAssertEqual(completionCounter, 1)
    }
}


fileprivate extension CGRect {
    func rounded(accuracy: CGFloat) -> CGRect {
        return CGRect(
            x: self.origin.x.rounded(accuracy: accuracy),
            y: self.origin.y.rounded(accuracy: accuracy),
            width: self.size.width.rounded(accuracy: accuracy),
            height: self.size.height.rounded(accuracy: accuracy)
        )
    }
}

fileprivate extension CGFloat {
    func rounded(accuracy: CGFloat) -> CGFloat {
        let inverseAccuracy = 1 / accuracy
        return (self * inverseAccuracy).rounded() / inverseAccuracy
    }
}
