# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

import json

from archery.benchmark.codec import JsonEncoder
from archery.benchmark.core import Benchmark, median
from archery.benchmark.compare import (
    BenchmarkComparator, RunnerComparator
)
from archery.benchmark.google import (
    GoogleBenchmark, GoogleBenchmarkObservation
)
from archery.benchmark.runner import StaticBenchmarkRunner


def test_benchmark_comparator():
    unit = "micros"

    assert not BenchmarkComparator(
        Benchmark("contender", unit, True, [10], unit, [1]),
        Benchmark("baseline", unit, True, [20], unit, [1]),
    ).regression

    assert BenchmarkComparator(
        Benchmark("contender", unit, False, [10], unit, [1]),
        Benchmark("baseline", unit, False, [20], unit, [1]),
    ).regression

    assert BenchmarkComparator(
        Benchmark("contender", unit, True, [20], unit, [1]),
        Benchmark("baseline", unit, True, [10], unit, [1]),
    ).regression

    assert not BenchmarkComparator(
        Benchmark("contender", unit, False, [20], unit, [1]),
        Benchmark("baseline", unit, False, [10], unit, [1]),
    ).regression


def test_static_runner_from_json_not_a_regression():
    archery_result = {
        "suites": [
            {
                "name": "arrow-value-parsing-benchmark",
                "benchmarks": [
                    {
                        "name": "FloatParsing<DoubleType>",
                        "unit": "items_per_second",
                        "less_is_better": False,
                        "values": [
                            109941112.87296811
                        ],
                        "time_unit": "ns",
                        "times": [
                            9095.800104330105
                        ]
                    },
                ]
            }
        ]
    }

    contender = StaticBenchmarkRunner.from_json(json.dumps(archery_result))
    baseline = StaticBenchmarkRunner.from_json(json.dumps(archery_result))
    [comparison] = RunnerComparator(contender, baseline).comparisons
    assert not comparison.regression


def test_static_runner_from_json_regression():
    archery_result = {
        "suites": [
            {
                "name": "arrow-value-parsing-benchmark",
                "benchmarks": [
                    {
                        "name": "FloatParsing<DoubleType>",
                        "unit": "items_per_second",
                        "less_is_better": False,
                        "values": [
                            109941112.87296811
                        ],
                        "time_unit": "ns",
                        "times": [
                            9095.800104330105
                        ]
                    },
                ]
            }
        ]
    }

    contender = StaticBenchmarkRunner.from_json(json.dumps(archery_result))

    # introduce artificial regression
    archery_result['suites'][0]['benchmarks'][0]['values'][0] *= 2
    baseline = StaticBenchmarkRunner.from_json(json.dumps(archery_result))

    [comparison] = RunnerComparator(contender, baseline).comparisons
    assert comparison.regression


def test_benchmark_median():
    assert median([10]) == 10
    assert median([1, 2, 3]) == 2
    assert median([1, 2]) == 1.5
    assert median([1, 2, 3, 4]) == 2.5
    assert median([1, 1, 1, 1]) == 1
    try:
        median([])
        assert False
    except ValueError:
        pass


def assert_benchmark(name, google_result, archery_result):
    observation = GoogleBenchmarkObservation(**google_result)
    benchmark = GoogleBenchmark(name, [observation])
    result = json.dumps(benchmark, cls=JsonEncoder)
    assert json.loads(result) == archery_result


def test_items_per_second():
    name = "ArrayArrayKernel<AddChecked, UInt8Type>/32768/0"
    google_result = {
        "cpu_time": 116292.58886653671,
        "items_per_second": 281772039.9844759,
        "iterations": 5964,
        "name": name,
        "null_percent": 0.0,
        "real_time": 119811.77313729875,
        "repetition_index": 0,
        "repetitions": 0,
        "run_name": name,
        "run_type": "iteration",
        "size": 32768.0,
        "threads": 1,
        "time_unit": "ns",
    }
    archery_result = {
        "counters": {"iterations": 5964,
                     "null_percent": 0.0,
                     "repetition_index": 0,
                     "repetitions": 0,
                     "run_name": name,
                     "threads": 1},
        "name": name,
        "unit": "items_per_second",
        "less_is_better": False,
        "values": [281772039.9844759],
        "time_unit": "ns",
        "times": [119811.77313729875],
    }
    assert "items_per_second" in google_result
    assert "bytes_per_second" not in google_result
    assert_benchmark(name, google_result, archery_result)


def test_bytes_per_second():
    name = "BufferOutputStreamLargeWrites/real_time"
    google_result = {
        "bytes_per_second": 1890209037.3405428,
        "cpu_time": 17018127.659574457,
        "iterations": 47,
        "name": name,
        "real_time": 17458386.53190963,
        "repetition_index": 1,
        "repetitions": 0,
        "run_name": name,
        "run_type": "iteration",
        "threads": 1,
        "time_unit": "ns",
    }
    archery_result = {
        "counters": {"iterations": 47,
                     "repetition_index": 1,
                     "repetitions": 0,
                     "run_name": name,
                     "threads": 1},
        "name": name,
        "unit": "bytes_per_second",
        "less_is_better": False,
        "values": [1890209037.3405428],
        "time_unit": "ns",
        "times": [17458386.53190963],
    }
    assert "items_per_second" not in google_result
    assert "bytes_per_second" in google_result
    assert_benchmark(name, google_result, archery_result)


def test_both_items_and_bytes_per_second():
    name = "ArrayArrayKernel<AddChecked, UInt8Type>/32768/0"
    google_result = {
        "bytes_per_second": 281772039.9844759,
        "cpu_time": 116292.58886653671,
        "items_per_second": 281772039.9844759,
        "iterations": 5964,
        "name": name,
        "null_percent": 0.0,
        "real_time": 119811.77313729875,
        "repetition_index": 0,
        "repetitions": 0,
        "run_name": name,
        "run_type": "iteration",
        "size": 32768.0,
        "threads": 1,
        "time_unit": "ns",
    }
    # Note that bytes_per_second trumps items_per_second
    archery_result = {
        "counters": {"iterations": 5964,
                     "null_percent": 0.0,
                     "repetition_index": 0,
                     "repetitions": 0,
                     "run_name": name,
                     "threads": 1},
        "name": name,
        "unit": "bytes_per_second",
        "less_is_better": False,
        "values": [281772039.9844759],
        "time_unit": "ns",
        "times": [119811.77313729875],
    }
    assert "items_per_second" in google_result
    assert "bytes_per_second" in google_result
    assert_benchmark(name, google_result, archery_result)


def test_neither_items_nor_bytes_per_second():
    name = "AllocateDeallocate<Jemalloc>/size:1048576/real_time"
    google_result = {
        "cpu_time": 1778.6004847419827,
        "iterations": 352765,
        "name": name,
        "real_time": 1835.3137357788837,
        "repetition_index": 0,
        "repetitions": 0,
        "run_name": name,
        "run_type": "iteration",
        "threads": 1,
        "time_unit": "ns",
    }
    archery_result = {
        "counters": {"iterations": 352765,
                     "repetition_index": 0,
                     "repetitions": 0,
                     "run_name": name,
                     "threads": 1},
        "name": name,
        "unit": "ns",
        "less_is_better": True,
        "values": [1835.3137357788837],
        "time_unit": "ns",
        "times": [1835.3137357788837],
    }
    assert "items_per_second" not in google_result
    assert "bytes_per_second" not in google_result
    assert_benchmark(name, google_result, archery_result)


def test_prefer_real_time():
    name = "AllocateDeallocate<Jemalloc>/size:1048576/real_time"
    google_result = {
        "cpu_time": 1778.6004847419827,
        "iterations": 352765,
        "name": name,
        "real_time": 1835.3137357788837,
        "repetition_index": 0,
        "repetitions": 0,
        "run_name": name,
        "run_type": "iteration",
        "threads": 1,
        "time_unit": "ns",
    }
    archery_result = {
        "counters": {"iterations": 352765,
                     "repetition_index": 0,
                     "repetitions": 0,
                     "run_name": name,
                     "threads": 1},
        "name": name,
        "unit": "ns",
        "less_is_better": True,
        "values": [1835.3137357788837],
        "time_unit": "ns",
        "times": [1835.3137357788837],
    }
    assert name.endswith("/real_time")
    assert_benchmark(name, google_result, archery_result)


def test_prefer_cpu_time():
    name = "AllocateDeallocate<Jemalloc>/size:1048576"
    google_result = {
        "cpu_time": 1778.6004847419827,
        "iterations": 352765,
        "name": name,
        "real_time": 1835.3137357788837,
        "repetition_index": 0,
        "repetitions": 0,
        "run_name": name,
        "run_type": "iteration",
        "threads": 1,
        "time_unit": "ns",
    }
    archery_result = {
        "counters": {"iterations": 352765,
                     "repetition_index": 0,
                     "repetitions": 0,
                     "run_name": name,
                     "threads": 1},
        "name": name,
        "unit": "ns",
        "less_is_better": True,
        "values": [1778.6004847419827],
        "time_unit": "ns",
        "times": [1835.3137357788837],
    }
    assert not name.endswith("/real_time")
    assert_benchmark(name, google_result, archery_result)


def test_omits_aggregates():
    name = "AllocateDeallocate<Jemalloc>/size:1048576/real_time"
    google_aggregate = {
        "aggregate_name": "mean",
        "cpu_time": 1757.428694267678,
        "iterations": 3,
        "name": "AllocateDeallocate<Jemalloc>/size:1048576/real_time_mean",
        "real_time": 1849.3869337041162,
        "repetitions": 0,
        "run_name": name,
        "run_type": "aggregate",
        "threads": 1,
        "time_unit": "ns",
    }
    google_result = {
        "cpu_time": 1778.6004847419827,
        "iterations": 352765,
        "name": name,
        "real_time": 1835.3137357788837,
        "repetition_index": 0,
        "repetitions": 0,
        "run_name": name,
        "run_type": "iteration",
        "threads": 1,
        "time_unit": "ns",
    }
    archery_result = {
        "counters": {"iterations": 352765,
                     "repetition_index": 0,
                     "repetitions": 0,
                     "run_name": name,
                     "threads": 1},
        "name": name,
        "unit": "ns",
        "less_is_better": True,
        "values": [1835.3137357788837],
        "time_unit": "ns",
        "times": [1835.3137357788837],
    }
    assert google_aggregate["run_type"] == "aggregate"
    assert google_result["run_type"] == "iteration"
    observation1 = GoogleBenchmarkObservation(**google_aggregate)
    observation2 = GoogleBenchmarkObservation(**google_result)
    benchmark = GoogleBenchmark(name, [observation1, observation2])
    result = json.dumps(benchmark, cls=JsonEncoder)
    assert json.loads(result) == archery_result
