import re

import numpy as np
import numpy.testing as npt
import pytest
from dataclasses import dataclass
import typing

from caimira import models

@dataclass(frozen=True)
class KnownConcentrationModelBase(models._ConcentrationModelBase):
    """
    A _ConcentrationModelBase class where all the class methods are
    redefined with a value taken from new parameters. Useful for testing.

    """
    known_population: models.Population

    known_removal_rate: float

    known_min_background_concentration: float

    known_normalization_factor: float

    @property
    def population(self) -> models.Population:
        return self.known_population

    def removal_rate(self, time: float) -> float:
        return self.known_removal_rate

    def min_background_concentration(self) -> float:
        return self.known_min_background_concentration

    def normalization_factor(self) -> float:
        return self.known_normalization_factor


@pytest.mark.parametrize(
    "override_params", [
        {'volume': np.array([100, 120])},
        {'humidity': np.array([0.5, 0.4])},
        {'air_change': np.array([100, 120])},
        {'viral_load_in_sputum': np.array([5e8, 1e9])},
    ]
)
def test_concentration_model_vectorisation(override_params):
    defaults = {
        'volume': 75,
        'humidity': 0.5,
        'air_change': 100,
        'viral_load_in_sputum': 1e9
    }
    defaults.update(override_params)

    always = models.PeriodicInterval(240, 240)  # TODO: This should be a thing on an interval.
    c_model = models.ConcentrationModel(
        models.Room(defaults['volume'], models.PiecewiseConstant((0., 24.), (293,)), defaults['humidity']),
        models.AirChange(always, defaults['air_change']),
        models.InfectedPopulation(
            number=1,
            presence=always,
            mask=models.Mask(
                factor_exhale=0.95,
                η_inhale=0.3,
            ),
            activity=models.Activity(
                0.51,
                0.75,
            ),
            virus=models.SARSCoV2(
                viral_load_in_sputum=defaults['viral_load_in_sputum'],
                infectious_dose=50.,
                viable_to_RNA_ratio = 0.5,
                transmissibility_factor=1.0,
            ),
            expiration=models._ExpirationBase.types['Breathing'],
            host_immunity=0.,
        ),
        evaporation_factor=0.3,
    )
    concentrations = c_model.concentration(10)
    assert isinstance(concentrations, np.ndarray)
    assert concentrations.shape == (2, )


@pytest.fixture
def simple_conc_model():
    interesting_times = models.SpecificInterval(([0.5, 1.], [1.1, 2], [2., 3.]), )
    return models.ConcentrationModel(
        room = models.Room(75, models.PiecewiseConstant((0., 24.), (293,))),
        ventilation = models.AirChange(interesting_times, 100),
        infected = models.InfectedPopulation(
            number=1,
            presence=interesting_times,
            mask=models.Mask.types['Type I'],
            activity=models.Activity.types['Seated'],
            virus=models.Virus.types['SARS_CoV_2'],
            expiration=models.Expiration.types['Breathing'],
            host_immunity=0.,
        ),
        evaporation_factor=0.3,
    )


@pytest.fixture
def dummy_population(simple_conc_model) -> models.Population:
    return models.Population(
        number=1,
        presence=simple_conc_model.infected.presence,
        mask=models.Mask.types['Type I'],
        activity=models.Activity.types['Seated'],
        host_immunity=0.,
    )


@pytest.mark.parametrize(
    "time, expected_last_state_change", [
        [-15., 0.],  # Out of range goes to the first state.
        [0., 0.],
        [0.5, 0.0],
        [0.51, 0.5],
        [1., 0.5],
        [1.05, 1.],
        [1.1, 1.],
        [1.11, 1.1],
        [2., 1.1],
        [2.1, 2],
        [3., 2],
        [15., 3.],  # Out of range goes to the last state.
    ]
)
def test_last_state_change_time(
        simple_conc_model: models.ConcentrationModel,
        time,
        expected_last_state_change,
):
    assert simple_conc_model.last_state_change(float(time)) == expected_last_state_change


@pytest.mark.parametrize(
    "time, expected_next_state_change", [
        [0.0, 0.0],
        [0.5, 0.5],
        [1, 1],
        [1.05, 1.1],
        [1.1, 1.1],
        [1.11, 2],
        [2, 2],
        [2.1, 3],
        [3, 3],
    ]
)
def test_next_state_change_time(
        simple_conc_model: models.ConcentrationModel,
        time,
        expected_next_state_change,
):
    assert simple_conc_model._next_state_change(float(time)) == expected_next_state_change


def test_next_state_change_time_out_of_range(simple_conc_model: models.ConcentrationModel):
    with pytest.raises(
            ValueError,
            match=re.escape("The requested time (3.1) is greater than last available state change time (3.0)")
    ):
        simple_conc_model._next_state_change(3.1)


def test_first_presence_time(simple_conc_model):
    assert simple_conc_model._first_presence_time() == 0.5


def test_integrated_concentration(simple_conc_model):
    c1 = simple_conc_model.integrated_concentration(0, 2)
    c2 = simple_conc_model.integrated_concentration(0, 1)
    c3 = simple_conc_model.integrated_concentration(1, 2)
    assert c1 != 0
    npt.assert_almost_equal(c1, c2 + c3, decimal=15)


# The expected numbers were obtained via the quad integration of the 
# normed_integrated_concentration method with 0 (start) and 2 (stop) as limits.
@pytest.mark.parametrize([
    "known_min_background_concentration", 
    "expected_normed_integrated_concentration"], 
    [
        [0.0, 0.00018533333708996207],
        [240.0, 48.000185340695275],
        [440.0, 88.00018534069527],
        [600., 120.00018534069527],
        [1000., 200.0001853407918],
    ]
)
def test_normed_integrated_concentration_with_background_concentration(
    simple_conc_model: models.ConcentrationModel,
    dummy_population: models.Population,
    known_min_background_concentration: float, 
    expected_normed_integrated_concentration: float):

    known_conc_model = KnownConcentrationModelBase(
        room = simple_conc_model.room, 
        ventilation = simple_conc_model.ventilation, 
        known_population = dummy_population,
        known_removal_rate = 100.,
        known_min_background_concentration = known_min_background_concentration,
        known_normalization_factor = 10.)    
    npt.assert_almost_equal(known_conc_model.normed_integrated_concentration(0, 2), expected_normed_integrated_concentration)


# The expected numbers were obtained via the quad integration of the 
# normed_integrated_concentration method with 0 (start) and 2 (stop) as limits.
@pytest.mark.parametrize([
    "known_removal_rate",
    "known_min_background_concentration", 
    "known_normalization_factor",
    "expected_normed_integrated_concentration"], 
    [
        [np.array([0.25, 10]), 0.0, 10., np.array([0.012161005755130391, 0.0017333437605308818])],
        [100, np.array([0, 240.0]), 10., np.array([0.00018533333708996207, 48.000185340695275])],
        [100, 440.0, np.array([10., 20.]), np.array([88.00018534069527, 44.000185340695275])],
        [np.array([10, 100]), np.array([600., 800.]), 10., np.array([120.00173334473946, 160.0001853406953])],
        [np.array([50, 100,]), np.array([1000.,1100.]), np.array([10., 20.]), np.array([200.00036800764332, 110.00018534069527])],
    ]
)
def test_normed_integrated_concentration_vectorisation(
    simple_conc_model: models.ConcentrationModel,
    dummy_population: models.Population,
    known_removal_rate: float,
    known_min_background_concentration: float, 
    known_normalization_factor: float,
    expected_normed_integrated_concentration: float):

    known_conc_model = KnownConcentrationModelBase(
        room = simple_conc_model.room, 
        ventilation = simple_conc_model.ventilation, 
        known_population = dummy_population,
        known_removal_rate = known_removal_rate,
        known_min_background_concentration = known_min_background_concentration,
        known_normalization_factor = known_normalization_factor)
    
    integrated_concentration = known_conc_model.normed_integrated_concentration(0, 2)

    assert isinstance(integrated_concentration, np.ndarray)
    assert integrated_concentration.shape == (2, )
    npt.assert_almost_equal(integrated_concentration, expected_normed_integrated_concentration)


@pytest.mark.parametrize([
    "known_removal_rate",
    "known_min_background_concentration", 
    "expected_concentration"], 
    [
        [0., 240., 240.],
        [0., np.array([240., 240.]), np.array([240., 240.])]
    ]
)
def test_zero_ventilation_rate(
    simple_conc_model: models.ConcentrationModel,
    dummy_population: models.Population,
    known_removal_rate: float,
    known_min_background_concentration: float,
    expected_concentration: float):

    known_conc_model = KnownConcentrationModelBase(
        room = simple_conc_model.room, 
        ventilation = simple_conc_model.ventilation, 
        known_population = dummy_population,
        known_removal_rate = known_removal_rate,
        known_normalization_factor=1.,
        known_min_background_concentration = known_min_background_concentration)
    
    normed_concentration = known_conc_model.concentration(1)
    npt.assert_almost_equal(normed_concentration, expected_concentration)
