import XCTest

class QuadKeyTests: XCTestCase {

    func testQuadKeys() {
        // Iterate over a good sampling of latitudes and longitudes at given zoom levels to verify our quad key calculations
        var p: QuadKeyPrecision = 1
        while p <= 32 { // iterate precisions
            var lat: QuadKeyDegrees = -90
            while lat <= 90 { // iterate latitudes
                var lon: QuadKeyDegrees = -180
                while lon <= 180 { // iterate longitudes
                    let latPart = QuadKeyPart(latitude: lat, precision: p) // calculate the latitude part (the y coordinate of the quad key)
                    let lonPart = QuadKeyPart(longitude: lon, precision: p) // calculate the longitude part (the x coordinate of the quad key)
                    let quadKey = QuadKey(latitudePart: latPart, longitudePart: lonPart, precision: p) // calculate the QuadKey given the two parts
                    let altQuadKey = QuadKey(latitude: lat, longitude: lon, precision: p) // calculate the quad key directly from lat and lon
                    XCTAssertEqual(quadKey, altQuadKey, "QuadKeys should match no matter how they were calculated")
                    let fullPrecisionLatPart = QuadKeyPart(latitude: lat)
                    let fullPrecisionLonPart = QuadKeyPart(longitude: lon)
                    let fullPrecisionQuadKey = QuadKey(latitude: lat, longitude: lon) // Calculate the full precision quad key
                    let adjustedQuadKey = fullPrecisionQuadKey.adjusted(downBy: 32 - p) // Adjust the quad key to the given zoom level (lose precision)
                    XCTAssertEqual(quadKey, adjustedQuadKey, "QuadKeys should match no matter how they were calculated\n\(adjustedQuadKey.bitmaskString)\n\(quadKey.bitmaskString)\n\(fullPrecisionQuadKey.bitmaskString)\n\n\(fullPrecisionLatPart.bitmaskString)\n\(latPart.bitmaskString)\n\n\(fullPrecisionLonPart.bitmaskString)\n\(lonPart.bitmaskString)")
                    let bounds = QuadKeyBounds(quadKey: adjustedQuadKey, precision: p)
                    XCTAssertGreaterThanOrEqual(fullPrecisionQuadKey, bounds.min)
                    XCTAssertLessThanOrEqual(fullPrecisionQuadKey, bounds.max)
                    let signedInteger = Int64(quadKey: fullPrecisionQuadKey)
                    let signedMax = Int64(quadKey: bounds.max)
                    let signedMin = Int64(quadKey: bounds.min)
                    XCTAssertGreaterThanOrEqual(signedInteger, signedMin)
                    XCTAssertLessThanOrEqual(signedInteger, signedMax)
                    XCTAssertEqual(QuadKey(int64: signedInteger), fullPrecisionQuadKey)
                    let coord = QuadKeyCoordinate(quadKey: quadKey, precision: p)
                    XCTAssertEqual(coord.latitudePart, latPart, "latPart: \(latPart) != \(coord.latitudePart)")
                    XCTAssertEqual(coord.longitudePart, lonPart, "lonPart: \(lonPart) != \(coord.longitudePart)")
                    let calculatedLat = QuadKeyDegrees(latitudePart: coord.latitudePart, precision: p)
                    let calculatedLon = QuadKeyDegrees(longitudePart: coord.longitudePart, precision: p)
                    XCTAssertEqual(calculatedLat, lat, accuracy: p.deltaLatitude, "latitude is wrong at precision \(p)")
                    XCTAssertEqual(calculatedLon, lon,  accuracy: p.deltaLongitude, "longitude is wrong at precision \(p)")
                    
                    if p == 32 {
                        let fullPrecision = QuadKeyPrecision(32)
                        let fullPrecisionCoord = QuadKeyCoordinate(quadKey: fullPrecisionQuadKey)
                        let fullPrecisionCalculatedLat = QuadKeyDegrees(latitudePart: fullPrecisionCoord.latitudePart)
                        let fullPrecisionCalculatedLon = QuadKeyDegrees(longitudePart: fullPrecisionCoord.longitudePart)
                        XCTAssertEqual(fullPrecisionCalculatedLat, lat, accuracy: fullPrecision.deltaLatitude, "full precision latitude is wrong")
                        XCTAssertEqual(fullPrecisionCalculatedLon, lon,  accuracy: fullPrecision.deltaLongitude, "full precision longitude is wrong")
                    }
                    lon += 5.25
                }
                lat += 5.25
            }
            p += (p == 1 ? 3 : 4)
        }

    }
    
    func testInvalidQuadKeys() {
        var invalidKey = QuadKey(latitude: Double.nan, longitude: Double.nan)
        XCTAssertEqual(invalidKey.latitude, -90)
        XCTAssertEqual(invalidKey.longitude, -180)
        
        invalidKey = QuadKey(latitude: Double.signalingNaN, longitude: Double.signalingNaN)
        XCTAssertEqual(invalidKey.latitude, -90)
        XCTAssertEqual(invalidKey.longitude, -180)
        
        invalidKey = QuadKey(latitude: Double.infinity, longitude: Double.infinity)
        XCTAssertEqual(invalidKey.latitude, -90)
        XCTAssertEqual(invalidKey.longitude, -180)
        
        invalidKey = QuadKey(latitude: 90.00001, longitude: 180.00001)
        XCTAssertEqual(invalidKey.longitude, 180)
        XCTAssertEqual(invalidKey.latitude, 90)
        
        invalidKey = QuadKey(latitude: -90.01, longitude: -180.01)
        XCTAssertEqual(invalidKey.longitude, -180)
        XCTAssertEqual(invalidKey.latitude, -90)
        
        var validKey = QuadKey(latitude: 90, longitude: 180)
        XCTAssertEqual(validKey.longitude, 180)
        XCTAssertEqual(validKey.latitude, 90)
        
        validKey = QuadKey(latitude: -90, longitude: -180)
        XCTAssertEqual(validKey.longitude, -180)
        XCTAssertEqual(validKey.latitude, -90)
        
        validKey = QuadKey(latitude: -0.0, longitude: -0.0)
        XCTAssertEqual(validKey.longitude, 0, accuracy: 0.00001)
        XCTAssertEqual(validKey.latitude, 0, accuracy: 0.00001)
    }
    
}
