import numpy as np
import numpy.testing as npt
import pytest

import cara.models as models
import cara.data as data


def test_no_mask_superspeading_emission_rate(baseline_concentration_model):
    expected_rate = 48500.
    npt.assert_allclose(
        [baseline_concentration_model.infected.emission_rate(float(t)) for t in [0, 1, 4, 4.5, 5, 8, 9]],
        [0, expected_rate, expected_rate, 0, 0, expected_rate, 0],
        rtol=1e-12
    )


@pytest.fixture
def baseline_periodic_window():
    return models.SlidingWindow(
        active=models.PeriodicInterval(period=120, duration=15),
        outside_temp=models.PiecewiseConstant((0., 24.), (283,)),
        window_height=1.6, opening_length=0.6,
    )


@pytest.fixture
def baseline_room():
    return models.Room(volume=75, inside_temp=models.PiecewiseConstant((0., 24.), (293,)))


@pytest.fixture
def baseline_periodic_hepa():
    return models.HEPAFilter(
        active=models.PeriodicInterval(period=120, duration=15),
        q_air_mech=514.74,
    )


def test_concentrations(baseline_concentration_model):
    # Expected concentrations were computed analytically
    ts = [0, 4, 5, 7, 10]
    concentrations = [baseline_concentration_model.concentration(float(t)) for t in ts]
    npt.assert_allclose(
        concentrations,
        [0.000000e+00, 2.046096e+01, 3.846725e-13, 2.046096e+01, 7.231966e-27],
        rtol=1e-6
    )


def test_smooth_concentrations(baseline_concentration_model):
    # We don't care about the actual concentrations in this test, but rather
    # that the curve itself is smooth.
    dx = 0.002
    dy_limit = 0.2  # Anything more than this (in relative) is a bit steep.
    ts = np.arange(0, 10, dx)
    concentrations = [baseline_concentration_model.concentration(float(t)) for t in ts]
    assert np.abs(np.diff(concentrations)).max()/np.mean(concentrations) < dy_limit


def build_model(interval_duration):
    model = models.ConcentrationModel(
        room=models.Room(volume=75),
        ventilation=models.HEPAFilter(
            active=models.PeriodicInterval(period=120, duration=interval_duration),
            q_air_mech=500.,
        ),
        infected=models.EmittingPopulation(
            number=1,
            virus=models.Virus.types['SARS_CoV_2'],
            presence=models.SpecificInterval(((0., 4.), (5., 8.))),
            mask=models.Mask.types['No mask'],
            activity=models.Activity.types['Light activity'],
            known_individual_emission_rate=970 * 50,
            host_immunity=0.,
            # Superspreading event, where ejection factor is fixed based
            # on Miller et al. (2020) - 50 represents the infectious dose.
        ),
        evaporation_factor=0.3,
    )
    return model


def test_concentrations_startup():
    # The concentrations should be the same until the beginning of the
    # first time that the ventilation is disabled.
    m1 = build_model(interval_duration=120)
    m2 = build_model(interval_duration=65)

    assert m1.concentration(1.) == m2.concentration(1.)


def test_r0(baseline_exposure_model):
    # Expected r0 was computed with a trapezoidal integration, using
    # a mesh of 100'000 pts per exposed presence interval.
    r0 = baseline_exposure_model.reproduction_number()
    npt.assert_allclose(r0, 771.380385)


def test_periodic_window(baseline_periodic_window, baseline_room):
    # Interesting transition times for a period of 120 and duration of 15.
    ts = [0, 14/60, 15/60, 16/60, 119/60, 120/60, 121/60, 239/60, 240/60]
    aes = [baseline_periodic_window.air_exchange(baseline_room, t) for t in ts]
    rate = 6.86347
    answers = [0, rate, rate, 0, 0, 0, rate, 0, 0]
    npt.assert_allclose(aes, answers, rtol=1e-5)


def test_periodic_hepa(baseline_periodic_hepa, baseline_room):
    # Interesting transition times for a period of 120 and duration of 15.
    ts = [0, 14 / 60, 15 / 60, 16 / 60, 119 / 60, 120 / 60, 121 / 60, 239 / 60, 240 / 60]
    rate = 514.74 / 75
    aes = [baseline_periodic_hepa.air_exchange(baseline_room, t) for t in ts]
    answers = [0, rate, rate, 0, 0, 0, rate, 0, 0]
    npt.assert_allclose(aes, answers, rtol=1e-5)


@pytest.mark.parametrize(
    "time, expected_value",
    [
        [0., 0.],
        [1 / 60, 514.74 / 68],
        [14 / 60, 514.74 / 68 + 5.3842316],
        [15 / 60, 514.74 / 68 + 5.3842316],
        [16 / 60, 5.3842316],
        [1., 5.3842316],
        [1.5, 3.7393925],
        [121 / 60, 514.74 / 68 + 3.7393925],
        [2.5, 3.7393925],
    ],
)
def test_multiple_ventilation_HEPA_window(baseline_periodic_hepa, time, expected_value):
    room = models.Room(volume=68., inside_temp=models.PiecewiseConstant((0., 24.),(293.15,)))
    tempOutside = models.PiecewiseConstant((0., 1., 2.5),(273.15, 283.15))
    window = models.SlidingWindow(active=models.SpecificInterval([(1 / 60, 24.)]),
                outside_temp=tempOutside,
                window_height=1.,opening_length=0.6)
    vent = models.MultipleVentilation([window, baseline_periodic_hepa])
    npt.assert_allclose(vent.air_exchange(room,time), expected_value, rtol=1e-5)


def test_multiple_ventilation_HEPA_window_transitions(baseline_periodic_hepa):
    tempOutside = models.PiecewiseConstant((0., 1., 2.5),(273.15, 283.15))
    room = models.Room(68, models.PiecewiseConstant((0., 24.),(293.15,)))
    window = models.SlidingWindow(active=models.SpecificInterval([(1 / 60, 24.)]),
                outside_temp=tempOutside,
                window_height=1.,opening_length=0.6)
    vent = models.MultipleVentilation([window, baseline_periodic_hepa])
    assert set(vent.transition_times(room)) == set([0.0, 1/60, 0.25, 1.0, 2.0, 2.25,
            2.5, 4.0, 4.25, 6.0, 6.25, 8.0, 8.25, 10.0, 10.25, 12.0, 12.25,
            14.0, 14.25, 16.0, 16.25, 18.0, 18.25, 20.0, 20.25, 22.0, 22.25, 24.])


@pytest.mark.parametrize(
    "volume, expected_value",
    [
        [24.5, 500 / 24.5 + 100 / 24.5 + 3.],
        [70, 500 / 70 + 100 / 70 + 3.],
    ],
)
def test_multiple_ventilation_HEPA_HVAC_AirChange(volume, expected_value):
    room = models.Room(volume=volume)
    hepa = models.HEPAFilter(
        active=models.SpecificInterval(((0,24),)),
        q_air_mech=500.,
    )
    hvac = models.HVACMechanical(
        active=models.SpecificInterval(((0,24),)),
        q_air_mech=100.,
    )
    airchange = models.AirChange(
        active=models.SpecificInterval(((0,24),)),
        air_exch=3.,
    )
    vent = models.MultipleVentilation([hepa, hvac, airchange])
    npt.assert_allclose(vent.air_exchange(room,10.),
                        expected_value,rtol=1e-5)


@pytest.mark.parametrize(
    "time, expected_value",
    [
        [8., 5.3842316],
        [16., 3.7393925],
    ],
)
def test_windowopening(time, expected_value):
    tempOutside = models.PiecewiseConstant((0., 10., 24.),(273.15, 283.15))
    w = models.SlidingWindow(
        active=models.SpecificInterval([(0., 24.)]),
        outside_temp=tempOutside,
        window_height=1., opening_length=0.6,
    )
    npt.assert_allclose(
        w.air_exchange(models.Room(volume=68, inside_temp=models.PiecewiseConstant((0., 24.), (293.15, ))), time), expected_value, rtol=1e-5
    )


def build_hourly_dependent_model(
        month,
        intervals_open=((7.5, 8.5),),
        intervals_presence_infected=((0., 4.), (5., 7.5)),
        artificial_refinement=False,
        temperatures=data.GenevaTemperatures_hourly
):
    if artificial_refinement:
        # 5-fold increase of number of times, WITHOUT interpolation
        # (hence transparent for the results)
        refine_factor = 2
        times_refined = tuple(
            float(t) for t in np.linspace(
                0., 24, refine_factor * len(temperatures[month].values) + 1
            )
        )
        temperatures_refined = tuple(np.hstack(
            [[v] * refine_factor for v in temperatures[month].values]
        ))
        outside_temp = models.PiecewiseConstant(times_refined, temperatures_refined)
    else:
        outside_temp = temperatures[month]

    model = models.ConcentrationModel(
        room=models.Room(volume=75, inside_temp=models.PiecewiseConstant((0., 24.), (293, ))),
        ventilation=models.SlidingWindow(
            active=models.SpecificInterval(intervals_open),
            outside_temp=outside_temp,
            window_height=1.6, opening_length=0.6,
        ),
        infected=models.EmittingPopulation(
            number=1,
            virus=models.Virus.types['SARS_CoV_2'],
            presence=models.SpecificInterval(intervals_presence_infected),
            mask=models.Mask.types['No mask'],
            activity=models.Activity.types['Light activity'],
            known_individual_emission_rate=970 * 50,
            host_immunity=0,
        ),
        evaporation_factor=0.3,
    )
    return model


def build_constant_temp_model(outside_temp, intervals_open=((7.5, 8.5),)):
    model = models.ConcentrationModel(
        room=models.Room(volume=75, inside_temp=models.PiecewiseConstant((0., 24.), (293,))),
        ventilation=models.SlidingWindow(
            active=models.SpecificInterval(intervals_open),
            outside_temp=models.PiecewiseConstant((0., 24.), (outside_temp,)),
            window_height=1.6, opening_length=0.6,
        ),
        infected=models.EmittingPopulation(
            number=1,
            virus=models.Virus.types['SARS_CoV_2'],
            presence=models.SpecificInterval(((0., 4.), (5., 7.5))),
            mask=models.Mask.types['No mask'],
            activity=models.Activity.types['Light activity'],
            known_individual_emission_rate=970 * 50,
            host_immunity=0.,
        ),
        evaporation_factor=0.3,
    )
    return model


def build_hourly_dependent_model_multipleventilation(month, intervals_open=((7.5, 8.5),)):
    vent = models.MultipleVentilation((
        models.SlidingWindow(
            active=models.SpecificInterval(intervals_open),
            outside_temp=data.GenevaTemperatures[month],
            window_height=1.6, opening_length=0.6,
        ),
        models.HEPAFilter(
            active=models.SpecificInterval(((0., 24.),)),
            q_air_mech=500.,
        ),
    ))
    model = models.ConcentrationModel(
        room=models.Room(volume=75, inside_temp=models.PiecewiseConstant((0., 24.), (293,))),
        ventilation=vent,
        infected=models.EmittingPopulation(
            number=1,
            virus=models.Virus.types['SARS_CoV_2'],
            presence=models.SpecificInterval(((0., 4.), (5., 7.5))),
            mask=models.Mask.types['No mask'],
            activity=models.Activity.types['Light activity'],
            known_individual_emission_rate=970 * 50,
            host_immunity=0.,
        ),
        evaporation_factor=0.3,
    )
    return model


@pytest.mark.parametrize(
    "month, temperatures",
    data.local_hourly_temperatures_celsius_per_hour.items(),
)
@pytest.mark.parametrize(
    "time",
    [0.5, 1.2, 2., 3.5, 5., 6.5, 7.5, 7.9, 8.],
)
def test_concentrations_hourly_dep_temp_vs_constant(month, temperatures, time):
    # The concentrations should be the same up to 8 AM (time when the
    # temperature changes DURING the window opening).
    m1 = build_hourly_dependent_model(month)
    m2 = build_constant_temp_model(temperatures[7] + 273.15)
    npt.assert_allclose(m1.concentration(time), m2.concentration(time), rtol=1e-5)

@pytest.mark.parametrize(
    "month, temperatures",
    data.local_hourly_temperatures_celsius_per_hour.items(),
)
@pytest.mark.parametrize(
    "time",
    [0.5, 1.2, 2., 3.5, 5., 6.5, 7.5, 7.9, 8.],
)
def test_concentrations_hourly_dep_temp_startup(month, temperatures, time):
    # The concentrations should be the zero up to the first presence time
    # of an infected person.
    m = build_hourly_dependent_model(
        month,
        ((0., 0.5), (1., 1.5), (4., 4.5), (7.5, 8), ),
        ((8., 12.), ),
    )
    assert m.concentration(time) == 0.


def test_concentrations_hourly_dep_multipleventilation():
    m = build_hourly_dependent_model_multipleventilation('Jan')
    m.concentration(12.)


@pytest.mark.parametrize(
    "month_temp_item",
    data.local_hourly_temperatures_celsius_per_hour.items(),
)
@pytest.mark.parametrize(
    "time",
    [0.5, 1.2, 2., 3.5, 5., 6.5, 7.5, 7.9, 8., 8.5, 9., 12.],
)
def test_concentrations_hourly_dep_adding_artificial_transitions(month_temp_item, time):
    month, temperatures = month_temp_item
    # Adding a second opening inside the first one should not change anything
    m1 = build_hourly_dependent_model(month, intervals_open=((7.5, 8.5), ))
    m2 = build_hourly_dependent_model(month, intervals_open=((7.5, 8.5), (8., 8.1), ))
    npt.assert_allclose(m1.concentration(time), m2.concentration(time), rtol=1e-5)


@pytest.mark.parametrize(
    "time",
    (
        [float(t) for t in np.random.random_sample(10) * 24.]  # type: ignore
        + [float(t) for t in np.arange(0, 24.5, 0.5)]
    ),
)
def test_concentrations_refine_times(time):
    month = 'Jan'
    m1 = build_hourly_dependent_model(month, intervals_open=((0., 24.),))
    m2 = build_hourly_dependent_model(month, intervals_open=((0., 24.),),
                                      artificial_refinement=True)
    npt.assert_allclose(m1.concentration(time), m2.concentration(time), rtol=1e-8)


def build_exposure_model(concentration_model, short_range_model):
    infected = concentration_model.infected
    return models.ExposureModel(
        concentration_model=concentration_model,
        short_range=short_range_model,
        exposed=models.Population(
            number=10,
            presence=infected.presence,
            activity=infected.activity,
            mask=infected.mask,
            host_immunity=0.,
        ),
    )


# Expected deposited exposure were computed with a trapezoidal integration, using
# a mesh of 100'000 pts per exposed presence interval.
@pytest.mark.parametrize(
    "month, expected_deposited_exposure",
    [
        ['Jan', 359.140499],
        ['Jun', 1385.917562],
    ],
)
def test_exposure_hourly_dep(month,expected_deposited_exposure, baseline_sr_model):
    m = build_exposure_model(
        build_hourly_dependent_model(
            month,
            intervals_open=((0., 24.), ),
            intervals_presence_infected=((8., 12.), (13., 17.))
        ), baseline_sr_model
    )
    deposited_exposure = m.deposited_exposure()
    npt.assert_allclose(deposited_exposure, expected_deposited_exposure)

# Expected deposited exposure were computed with a trapezoidal integration, using
# a mesh of 100'000 pts per exposed presence interval and 25 pts per hour
# for the temperature discretization.
@pytest.mark.parametrize(
    "month, expected_deposited_exposure",
    [
        ['Jan', 359.983716],
        ['Jun', 1439.267381],
    ],
)
def test_exposure_hourly_dep_refined(month,expected_deposited_exposure, baseline_sr_model):
    m = build_exposure_model(
        build_hourly_dependent_model(
            month,
            intervals_open=((0., 24.),),
            intervals_presence_infected=((8., 12.), (13., 17.)),
            temperatures=data.GenevaTemperatures,
        ), baseline_sr_model
    )
    deposited_exposure = m.deposited_exposure()
    npt.assert_allclose(deposited_exposure, expected_deposited_exposure, rtol=0.02)
