from copy import deepcopy
from datetime import datetime, timedelta
import json
from pathlib import Path

from dandischema.consts import DANDI_SCHEMA_VERSION
from dandischema.metadata import validate
from dandischema.models import AgeReferenceType
from dandischema.models import BareAsset as BareAssetMeta
from dandischema.models import Dandiset as DandisetMeta
from dandischema.models import PropertyValue
from dateutil.tz import tzutc
import pytest
from semantic_version import Version

from ..metadata import (
    extract_age,
    get_metadata,
    metadata2asset,
    parse_age,
    timedelta2duration,
)
from ..pynwb_utils import metadata_nwb_subject_fields

METADATA_DIR = Path(__file__).with_name("data") / "metadata"


def test_get_metadata(simple1_nwb, simple1_nwb_metadata):
    target_metadata = simple1_nwb_metadata.copy()
    # we will also get some counts
    target_metadata["number_of_electrodes"] = 0
    target_metadata["number_of_units"] = 0
    target_metadata["number_of_units"] = 0
    # We also populate with nd_types now, although here they would be empty
    target_metadata["nd_types"] = []
    # we do not populate any subject fields in our simple1_nwb
    for f in metadata_nwb_subject_fields:
        target_metadata[f] = None
    metadata = get_metadata(str(simple1_nwb))
    # we also load nwb_version field, so it must not be degenerate and ATM
    # it is 2.X.Y. And since I don't know how to query pynwb on what
    # version it currently "supports", we will just pop it
    assert metadata.pop("nwb_version").startswith("2.")
    assert target_metadata == metadata


@pytest.mark.parametrize(
    "age,duration",
    [
        ("5y", "P5Y"),
        ("5Y", "P5Y"),
        ("5 years", "P5Y"),
        ("1year", "P1Y"),
        ("0y", "P0Y"),
        ("2 months", "P2M"),
        ("2 M", "P2M"),
        ("2 m", "P2M"),
        ("2M", "P2M"),
        ("2m", "P2M"),
        ("2mo", "P2M"),
        ("3 weeks", "P3W"),
        ("3 w", "P3W"),
        ("3 W", "P3W"),
        ("3w", "P3W"),
        ("3W", "P3W"),
        ("0 days", "P0D"),
        ("7 d", "P7D"),
        ("7 D", "P7D"),
        ("7d", "P7D"),
        ("7D", "P7D"),
        ("7 day", "P7D"),
        ("7 Days", "P7D"),
        ("7.5 Days", "P7.5D"),
        ("7.0 Days", "P7D"),
        ("P136D", "P136D"),
        ("P22265.0D", "P22265.0D"),  # "P22265.D" is not allowed
        ("P22265,0D", "P22265.0D"),
        ("P2DT10H20M", "P2DT10H20M"),
        ("P2DT10.5H", "P2DT10.5H"),
        ("P2DT10,5H", "P2DT10.5H"),
        ("349 days, 4 hours", "P349DT4H"),
        ("4 days, 4.5 hours", "P4DT4.5H"),
        ("12 weeks, 13 d; 10 hours, 30 min 1sec", "P12W13DT10H30M1S"),
        ("342 days, 4:30:02", "P342DT4H30M2S"),
        ("342 days, 00:00:00", "P342DT0H0M0S"),
        ("14 (Units: days)", "P14D"),
        ("14 unit day", "P14D"),
        ("Gestational Week 19", ("P19W", "Gestational")),
    ],
)
def test_parse_age(age, duration):
    if isinstance(duration, tuple):
        duration, ref = duration
    else:  # birth will be a default ref
        ref = "Birth"
    assert parse_age(age) == (duration, ref)


@pytest.mark.parametrize(
    "age, match_er",
    [
        ("123", "no rules to convert: 123"),
        ("P12", "ISO 8601 expected, but P12 was received"),
        ("3-7 months", "no rules to convert: 3-7 months"),
        ("3 months, some extra", "no rules to convert: some extra"),
        (" , ", "age doesn't have any information"),
        ("", "age is empty"),
        (None, "age is empty"),
        ("P2DT10.5H10M", "decimal fraction allowed in the lowest order"),
        ("4.5 hours 10 sec", "decimal fraction allowed in the lowest order"),
    ],
)
def test_parse_error(age, match_er):
    with pytest.raises(ValueError, match=match_er):
        parse_age(age)


@pytest.mark.parametrize(
    "td,duration",
    [
        (timedelta(), "P0D"),
        (timedelta(weeks=3), "P21D"),
        (timedelta(seconds=42), "PT42S"),
        (timedelta(days=5, seconds=23, microseconds=2000), "P5DT23.002S"),
    ],
)
def test_timedelta2duration(td, duration):
    assert timedelta2duration(td) == duration


@pytest.mark.parametrize(
    "filename, metadata",
    [
        (
            "metadata2asset.json",
            {
                "contentSize": 69105,
                "digest": "e455839e5ab2fa659861f58a423fd17f-1",
                "digest_type": "dandi_etag",
                "encodingFormat": "application/x-nwb",
                "experiment_description": "Experiment Description",
                "experimenter": "Joe Q. Experimenter",
                "id": "dandiasset:0b0a1a0b-e3ea-4cf6-be94-e02c830d54be",
                "institution": "University College",
                "keywords": ["test", "sample", "example", "test-case"],
                "lab": "Retriever Laboratory",
                "related_publications": "A Brief History of Test Cases",
                "session_description": "Some test data",
                "session_id": "XYZ789",
                "session_start_time": "2020-08-31T15:58:28-04:00",
                "age": "23 days",
                "date_of_birth": "2020-03-14T12:34:56-04:00",
                "genotype": "Typical",
                "sex": "M",
                "species": "human",
                "subject_id": "a1b2c3",
                "cell_id": "cell01",
                "slice_id": "slice02",
                "tissue_sample_id": "tissue03",
                "probe_ids": "probe04",
                "number_of_electrodes": 42,
                "number_of_units": 6,
                "nwb_version": "2.2.5",
                "nd_types": [
                    "Device (2)",
                    "DynamicTable",
                    "ElectricalSeries",
                    "ElectrodeGroup",
                    "Subject",
                ],
                "path": "/test/path",
            },
        ),
        (
            "metadata2asset_simple1.json",
            {
                "contentSize": 69105,
                "digest": "e455839e5ab2fa659861f58a423fd17f-1",
                "digest_type": "dandi_etag",
                "encodingFormat": "application/x-nwb",
                "nwb_version": "2.2.5",
                "experiment_description": "experiment_description1",
                "experimenter": ("experimenter1",),
                "id": "dandiasset:bfc23fb6192b41c083a7257e09a3702b",
                "institution": "institution1",
                "keywords": ["keyword1", "keyword 2"],
                "lab": "lab1",
                "related_publications": ("related_publications1",),
                "session_description": "session_description1",
                "session_id": "session_id1",
                "session_start_time": datetime(2017, 4, 15, 12, 0, tzinfo=tzutc()),
                "age": None,
                "date_of_birth": None,
                "genotype": None,
                "sex": None,
                "species": None,
                "subject_id": "sub-01",
                "number_of_electrodes": 0,
                "number_of_units": 0,
                "nd_types": [],
                "tissue_sample_id": "tissue42",
                "path": "/test/path",
            },
        ),
    ],
)
def test_metadata2asset(filename, metadata):
    data = metadata2asset(metadata)
    with (METADATA_DIR / filename).open() as fp:
        data_as_dict = json.load(fp)
    data_as_dict["schemaVersion"] = DANDI_SCHEMA_VERSION
    assert data == BareAssetMeta(**data_as_dict)
    bare_dict = deepcopy(data_as_dict)
    assert data.json_dict() == bare_dict
    data_as_dict["identifier"] = "0b0a1a0b-e3ea-4cf6-be94-e02c830d54be"
    # as of schema-0.5.0 (https://github.com/dandi/dandischema/pull/52)
    # contentUrl is required, and validate below would map into Asset,
    # due to schemaKey
    if Version(DANDI_SCHEMA_VERSION) >= Version("0.5.0"):
        data_as_dict["contentUrl"] = ["http://example.com"]
    validate(data_as_dict)


def test_dandimeta_migration():
    with (METADATA_DIR / "dandimeta_migration.new.json").open() as fp:
        data_as_dict = json.load(fp)
    data_as_dict["schemaVersion"] = DANDI_SCHEMA_VERSION
    DandisetMeta(**data_as_dict)
    validate(data_as_dict)


def test_time_extract():
    # if metadata contains date_of_birth and session_start_time,
    # age will be calculated from the values
    meta_birth = {
        "session_start_time": "2020-08-31T12:21:28-04:00",
        "age": "31 days",
        "date_of_birth": "2020-07-31T12:20:00-04:00",
    }
    age_birth = extract_age(meta_birth)
    assert age_birth.value == "P31DT88S"
    assert age_birth.valueReference == PropertyValue(
        value=AgeReferenceType("dandi:BirthReference")
    )

    # if metadata doesn't contain date_of_birth, the age field will be used
    meta = {"session_start_time": "2020-08-31T12:21:28-04:00", "age": "31 days"}
    age = extract_age(meta)
    assert age.value == "P31D"
    assert age.valueReference == PropertyValue(
        value=AgeReferenceType("dandi:BirthReference")
    )


def test_time_extract_gest():
    """extract age with Gestational ref"""
    meta_birth = {
        "session_start_time": "2020-08-31T12:21:28-04:00",
        "age": "Gestational week 3",
    }
    age_birth = extract_age(meta_birth)
    assert age_birth.value == "P3W"
    assert age_birth.valueReference == PropertyValue(
        value=AgeReferenceType("dandi:GestationalReference")
    )
