import typing

import pytest

import hpotk

from hpotk.model import Identified, ObservableFeature, TermId
from hpotk.validate import ValidationResult, ValidationLevel
from hpotk.validate import AnnotationPropagationValidator, PhenotypicAbnormalityValidator, ObsoleteTermIdsValidator



class SimpleFeature(Identified, ObservableFeature):

    def __init__(self, curie: str, status: bool):
        self._id = TermId.from_curie(curie)
        self._status = status

    @property
    def identifier(self) -> TermId:
        return self._id

    @property
    def is_present(self) -> bool:
        return self._status


@pytest.fixture
def example_terms() -> typing.Sequence[SimpleFeature]:
    return (
        SimpleFeature('HP:0001166', True),  # Arachnodactyly
        SimpleFeature('HP:0001250', True),  # Seizure
        SimpleFeature('HP:0032648', True),  # Tubularization of Bowman capsule
        SimpleFeature('HP:0000805', False)  # excluded Enuresis
    )


class TestAnnotationPropagationValidator:

    @pytest.fixture
    def validator(self, toy_hpo: hpotk.MinimalOntology) -> AnnotationPropagationValidator:
        return AnnotationPropagationValidator(toy_hpo)

    def test_empty_input_is_allowed(self, validator: AnnotationPropagationValidator):
        results = validator.validate([])
        assert results.is_ok()

    def test_ok_input_produces_no_errors(self, validator: AnnotationPropagationValidator,
                                         example_terms: typing.Sequence[SimpleFeature]):
        results = validator.validate(example_terms)
        assert results.is_ok()

    def test_obsolete_ancestor_produces_error(self, validator: AnnotationPropagationValidator,
                                              example_terms: typing.Sequence[SimpleFeature]):
        example_terms = list(example_terms)
        example_terms.append(TermId.from_curie('HP:0006010'))

        results = validator.validate(example_terms)

        assert not results.is_ok()
        assert results.results[0] == ValidationResult(level=ValidationLevel.ERROR, category='annotation_propagation',
                                                      message='Terms should not contain both present Arachnodactyly [HP:0001166] '
                                                              f'and its present or excluded ancestor Long fingers [HP:0100807]')

    @pytest.mark.parametrize('ancestor_curie, ancestor_status, base_curie, base_status',
                             (
                                     # present Long fingers vs. present Arachnodactyly
                                     ("HP:0100807", True, 'HP:0001166', True),

                                     # excluded Long fingers vs. present Arachnodactyly
                                     ("HP:0100807", False, 'HP:0001166', True),

                                     # The case is commented out since it is possible to have some abnormality but excluded specific abnormality
                                     # ("HP:0000014", True, 'HP:0000805', False),  # present Abnormality of the bladder vs. excluded Enuresis

                                     # excluded Abnormality of the bladder vs. excluded Enuresis
                                     ("HP:0000014", False, 'HP:0000805', False),
                             ))
    def test_ancestor_presence_produces_error(self, ancestor_curie, ancestor_status, base_curie, base_status,
                                              toy_hpo: hpotk.MinimalOntology, validator: AnnotationPropagationValidator,
                                              example_terms: typing.Sequence[SimpleFeature]):
        example_terms = list(example_terms)
        example_terms.append(SimpleFeature(ancestor_curie, ancestor_status))

        results = validator.validate(example_terms)

        assert not results.is_ok()
        assert 1 == len(results.results)
        first = results.results[0]
        assert ValidationLevel.ERROR == first.level
        assert 'annotation_propagation' == first.category

        state = 'present' if base_status else 'excluded'
        assert (f'Terms should not contain both {state} '
                f'{toy_hpo.get_term(base_curie).name} [{toy_hpo.get_term(base_curie).identifier.value}] '
                f'and its present or excluded ancestor '
                f'{toy_hpo.get_term(ancestor_curie).name} [{ancestor_curie}]' == first.message)


class TestPhenotypicAbnormalityValidator:

    @pytest.fixture
    def validator(self, toy_hpo: hpotk.MinimalOntology) -> PhenotypicAbnormalityValidator:
        return PhenotypicAbnormalityValidator(toy_hpo)

    def test_ok_input_produces_no_errors(self, validator: PhenotypicAbnormalityValidator,
                                         example_terms: typing.Sequence[SimpleFeature]):
        results = validator.validate(example_terms)
        assert results.is_ok()

    @pytest.mark.parametrize('curie',
                             (
                                     "HP:0012823",  # Clinical modifier
                                     "HP:0003825",  # Variable expressivity
                                     "HP:0003621",  # Juvenile onset
                             )
                             )
    def test_presence_of_clinical_modifier_produces_error(self, curie,
                                                          validator: PhenotypicAbnormalityValidator,
                                                          toy_hpo: hpotk.MinimalOntology,
                                                          example_terms: typing.Sequence[SimpleFeature]):
        example_terms = list(example_terms)
        example_terms.append(SimpleFeature(curie, True))
        results = validator.validate(example_terms)

        assert not results.is_ok()
        assert results.results[0] == ValidationResult(level=ValidationLevel.WARNING,
                                                      category='phenotypic_abnormality_descendant',
                                                      message=f'{toy_hpo.get_term(curie).name} '
                                                              f'[{toy_hpo.get_term(curie).identifier.value}] '
                                                              f'is not a descendant of '
                                                              'Phenotypic abnormality [HP:0000118]')


class TestObsoleteTermIdsValidator:

    @pytest.fixture
    def validator(self, toy_hpo: hpotk.MinimalOntology) -> ObsoleteTermIdsValidator:
        return ObsoleteTermIdsValidator(toy_hpo)

    def test_ok_input_produces_no_errors(self, validator: ObsoleteTermIdsValidator,
                                         example_terms: typing.Sequence[SimpleFeature]):
        results = validator.validate(example_terms)

        assert results.is_ok()

    def test_presence_of_an_obsolete_term_produces_error(self, validator: ObsoleteTermIdsValidator,
                                                         toy_hpo: hpotk.MinimalOntology,
                                                         example_terms: typing.Sequence[SimpleFeature]):
        curie = 'HP:0006010'  # obsolete Long fingers
        example_terms = list(example_terms)
        example_terms.append(SimpleFeature(curie, True))
        results = validator.validate(example_terms)

        assert not results.is_ok()
        assert results.results[0] == ValidationResult(level=ValidationLevel.WARNING,
                                                      category='obsolete_term_id_is_used',
                                                      message=f'Using the obsolete {curie} instead of '
                                                              f'{toy_hpo.get_term(curie).identifier.value} '
                                                              f'for {toy_hpo.get_term(curie).name}')
