//
//  GlucoseMathTests.swift
//  GlucoseKitTests
//
//  Created by Nathan Racklyeft on 1/24/16.
//  Copyright © 2016 Nathan Racklyeft. All rights reserved.
//

import XCTest
@testable import LoopKit
import HealthKit


public struct GlucoseFixtureValue: GlucoseSampleValue {
    public let startDate: Date
    public let quantity: HKQuantity
    public let isDisplayOnly: Bool
    public let wasUserEntered: Bool
    public let provenanceIdentifier: String
    public let condition: GlucoseCondition?
    public let trendRate: HKQuantity?

    public init(startDate: Date, quantity: HKQuantity, isDisplayOnly: Bool, wasUserEntered: Bool, provenanceIdentifier: String?, condition: GlucoseCondition?, trendRate: HKQuantity?) {
        self.startDate = startDate
        self.quantity = quantity
        self.isDisplayOnly = isDisplayOnly
        self.wasUserEntered = wasUserEntered
        self.provenanceIdentifier = provenanceIdentifier ?? "com.loopkit.LoopKitTests"
        self.condition = condition
        self.trendRate = trendRate
    }
}


extension GlucoseFixtureValue: Comparable {
    public static func <(lhs: GlucoseFixtureValue, rhs: GlucoseFixtureValue) -> Bool {
        return lhs.startDate < rhs.startDate
    }

    public static func ==(lhs: GlucoseFixtureValue, rhs: GlucoseFixtureValue) -> Bool {
        return lhs.startDate == rhs.startDate &&
            lhs.quantity == rhs.quantity &&
            lhs.isDisplayOnly == rhs.isDisplayOnly &&
            lhs.wasUserEntered == rhs.wasUserEntered &&
            lhs.provenanceIdentifier == rhs.provenanceIdentifier &&
            lhs.condition == rhs.condition &&
            lhs.trendRate == rhs.trendRate
    }
}


class GlucoseMathTests: XCTestCase {

    private func printFixture(_ effectVelocity: [GlucoseEffectVelocity]) {
        let formatter = ISO8601DateFormatter.localTimeDate()
        let unit = HKUnit.milligramsPerDeciliter.unitDivided(by: .minute())

        print("\n\n")
        print(String(data: try! JSONSerialization.data(
            withJSONObject: effectVelocity.map({ (value) -> [String: Any] in
                return [
                    "startDate": formatter.string(from: value.startDate),
                    "endDate": formatter.string(from: value.endDate),
                    "value": value.quantity.doubleValue(for: unit),
                    "unit": unit.unitString
                ]
            }),
            options: .prettyPrinted), encoding: .utf8)!)
        print("\n\n")
    }

    func loadInputFixture(_ resourceName: String) -> [GlucoseFixtureValue] {
        let fixture: [JSONDictionary] = loadFixture(resourceName)
        let dateFormatter = ISO8601DateFormatter.localTimeDate()

        return fixture.map {
            return GlucoseFixtureValue(
                startDate: dateFormatter.date(from: $0["date"] as! String)!,
                quantity: HKQuantity(unit: HKUnit.milligramsPerDeciliter, doubleValue: $0["amount"] as! Double),
                isDisplayOnly: ($0["display_only"] as? Bool) ?? false,
                wasUserEntered: ($0["user_entered"] as? Bool) ?? false,
                provenanceIdentifier: $0["provenance_identifier"] as? String,
                condition: ($0["condition"] as? String).flatMap { GlucoseCondition(rawValue: $0) },
                trendRate: ($0["trend_rate"] as? Double).flatMap { HKQuantity(unit: .milligramsPerDeciliter, doubleValue: $0) }
            )
        }
    }

    func loadOutputFixture(_ resourceName: String) -> [GlucoseEffect] {
        let fixture: [JSONDictionary] = loadFixture(resourceName)
        let dateFormatter = ISO8601DateFormatter.localTimeDate()

        return fixture.map {
            return GlucoseEffect(startDate: dateFormatter.date(from: $0["date"] as! String)!, quantity: HKQuantity(unit: HKUnit(from: $0["unit"] as! String), doubleValue: $0["amount"] as! Double))
        }
    }

    func loadEffectVelocityFixture(_ resourceName: String) -> [GlucoseEffectVelocity] {
        let fixture: [JSONDictionary] = loadFixture(resourceName)
        let dateFormatter = ISO8601DateFormatter.localTimeDate()

        return fixture.map {
            return GlucoseEffectVelocity(startDate: dateFormatter.date(from: $0["startDate"] as! String)!, endDate: dateFormatter.date(from: $0["endDate"] as! String)!, quantity: HKQuantity(unit: HKUnit(from: $0["unit"] as! String), doubleValue:$0["value"] as! Double))
        }
    }
    
    func testMomentumEffectForBouncingGlucose() {
        let input = loadInputFixture("momentum_effect_bouncing_glucose_input")
        let output = loadOutputFixture("momentum_effect_bouncing_glucose_output")

        let effects = input.linearMomentumEffect()
        let unit = HKUnit.milligramsPerDeciliter

        XCTAssertEqual(output.count, effects.count)

        for (expected, calculated) in zip(output, effects) {
            XCTAssertEqual(expected.startDate, calculated.startDate)
            XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
        }
    }

    func testMomentumEffectForRisingGlucose() {
        let input = loadInputFixture("momentum_effect_rising_glucose_input")
        let output = loadOutputFixture("momentum_effect_rising_glucose_output")

        let effects = input.linearMomentumEffect()
        let unit = HKUnit.milligramsPerDeciliter

        XCTAssertEqual(output.count, effects.count)

        for (expected, calculated) in zip(output, effects) {
            XCTAssertEqual(expected.startDate, calculated.startDate)
            XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
        }
    }

    func testMomentumEffectForRisingGlucoseDoubles() {
        let input = loadInputFixture("momentum_effect_rising_glucose_double_entries_input")
        let output = loadOutputFixture("momentum_effect_rising_glucose_output")

        let effects = input.linearMomentumEffect()
        let unit = HKUnit.milligramsPerDeciliter

        XCTAssertEqual(output.count, effects.count)

        for (expected, calculated) in zip(output, effects) {
            XCTAssertEqual(expected.startDate, calculated.startDate)
            XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
        }
    }

    func testMomentumEffectForFallingGlucose() {
        let input = loadInputFixture("momentum_effect_falling_glucose_input")
        let output = loadOutputFixture("momentum_effect_falling_glucose_output")

        let effects = input.linearMomentumEffect()
        let unit = HKUnit.milligramsPerDeciliter

        XCTAssertEqual(output.count, effects.count)

        for (expected, calculated) in zip(output, effects) {
            XCTAssertEqual(expected.startDate, calculated.startDate)
            XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
        }
    }

    func testMomentumEffectForFallingGlucoseDuplicates() {
        var input = loadInputFixture("momentum_effect_falling_glucose_input")
        let output = loadOutputFixture("momentum_effect_falling_glucose_output")
        input.append(contentsOf: input)
        input.sort(by: <)

        let effects = input.linearMomentumEffect()
        let unit = HKUnit.milligramsPerDeciliter

        XCTAssertEqual(output.count, effects.count)

        for (expected, calculated) in zip(output, effects) {
            XCTAssertEqual(expected.startDate, calculated.startDate)
            XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
        }
    }

    func testMomentumEffectForStableGlucose() {
        let input = loadInputFixture("momentum_effect_stable_glucose_input")
        let output = loadOutputFixture("momentum_effect_stable_glucose_output")

        let effects = input.linearMomentumEffect()
        let unit = HKUnit.milligramsPerDeciliter

        XCTAssertEqual(output.count, effects.count)

        for (expected, calculated) in zip(output, effects) {
            XCTAssertEqual(expected.startDate, calculated.startDate)
            XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
        }
    }

    func testMomentumEffectForDuplicateGlucose() {
        let input = loadInputFixture("momentum_effect_duplicate_glucose_input")
        let effects = input.linearMomentumEffect()

        XCTAssertEqual(0, effects.count)
    }

    func testMomentumEffectForEmptyGlucose() {
        let input = [GlucoseFixtureValue]()
        let effects = input.linearMomentumEffect()

        XCTAssertEqual(0, effects.count)
    }

    func testMomentumEffectForSpacedOutGlucose() {
        let input = loadInputFixture("momentum_effect_incomplete_glucose_input")
        let effects = input.linearMomentumEffect()

        XCTAssertEqual(0, effects.count)
    }

    func testMomentumEffectForTooFewGlucose() {
        let input = loadInputFixture("momentum_effect_bouncing_glucose_input")[0...1]
        let effects = input.linearMomentumEffect()

        XCTAssertEqual(0, effects.count)
    }

    func testMomentumEffectForDisplayOnlyGlucose() {
        let input = loadInputFixture("momentum_effect_display_only_glucose_input")
        let effects = input.linearMomentumEffect()

        XCTAssertEqual(0, effects.count)
    }

    func testMomentumEffectForMixedProvenanceGlucose() {
        let input = loadInputFixture("momentum_effect_mixed_provenance_glucose_input")
        let effects = input.linearMomentumEffect()

        XCTAssertEqual(0, effects.count)
    }

    func testCounteractionEffectsForFallingGlucose() {
        let input = loadInputFixture("counteraction_effect_falling_glucose_input")
        let insulinEffect = loadOutputFixture("counteraction_effect_falling_glucose_insulin")
        let output = loadEffectVelocityFixture("counteraction_effect_falling_glucose_output")

        let effects = input.counteractionEffects(to: insulinEffect)
        let unit = HKUnit.milligramsPerDeciliter.unitDivided(by: .minute())

        XCTAssertEqual(output.count, effects.count)

        for (expected, calculated) in zip(output, effects) {
            XCTAssertEqual(expected.startDate, calculated.startDate)
            XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
        }
    }

    func testCounteractionEffectsForFallingGlucoseDuplicates() {
        var input = loadInputFixture("counteraction_effect_falling_glucose_input")
        input.append(contentsOf: input)
        input.sort(by: <)
        let insulinEffect = loadOutputFixture("counteraction_effect_falling_glucose_insulin")
        let output = loadEffectVelocityFixture("counteraction_effect_falling_glucose_output")

        let effects = input.counteractionEffects(to: insulinEffect)
        let unit = HKUnit.milligramsPerDeciliter.unitDivided(by: .minute())

        XCTAssertEqual(output.count, effects.count)

        for (expected, calculated) in zip(output, effects) {
            XCTAssertEqual(expected.startDate, calculated.startDate)
            XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
        }
    }

    func testCounteractionEffectsForFallingGlucoseAlmostDuplicates() {
        let input = loadInputFixture("counteraction_effect_falling_glucose_almost_duplicates_input")
        let insulinEffect = loadOutputFixture("counteraction_effect_falling_glucose_insulin")
        let output = loadEffectVelocityFixture("counteraction_effect_falling_glucose_almost_duplicates_output")

        let effects = input.counteractionEffects(to: insulinEffect)
        let unit = HKUnit.milligramsPerDeciliter.unitDivided(by: .minute())

        XCTAssertEqual(output.count, effects.count)

        for (expected, calculated) in zip(output, effects) {
            XCTAssertEqual(expected.startDate, calculated.startDate)
            XCTAssertEqual(expected.endDate, calculated.endDate)
            XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
        }
    }

    func testCounteractionEffectsForNoGlucose() {
        let input = [GlucoseFixtureValue]()
        let insulinEffect = loadOutputFixture("counteraction_effect_falling_glucose_insulin")
        let output = [GlucoseEffectVelocity]()

        let effects = input.counteractionEffects(to: insulinEffect)

        XCTAssertEqual(output.count, effects.count)
    }
    
    func testMomentumEffectWithVelocityLimit() {
        let input = loadInputFixture("momentum_effect_impossible_rising_glucose_input")
        let output = loadOutputFixture("momentum_effect_impossible_rising_glucose_output")

        let effects = input.linearMomentumEffect()
        let unit = HKUnit.milligramsPerDeciliter

        XCTAssertEqual(output.count, effects.count)

        for (expected, calculated) in zip(output, effects) {
            XCTAssertEqual(expected.startDate, calculated.startDate)
            XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
        }
    }
}
