import platform
from unittest import TestCase

import pytest
import numpy as np
from hdmf.common import DynamicTable, DynamicTableRegion
from pynwb.file import TimeIntervals

from nwbinspector import (
    check_empty_table,
    check_time_interval_time_columns,
    check_time_intervals_stop_after_start,
    check_dynamic_table_region_data_validity,
    check_column_binary_capability,
    check_single_row,
)
from nwbinspector.register_checks import InspectorMessage, Importance


class TestCheckDynamicTableRegion(TestCase):
    def setUp(self):
        self.table = DynamicTable(name="test_table", description="")
        self.table.add_column(name="test_column", description="")
        for _ in range(10):
            self.table.add_row(test_column=1)

    def test_check_dynamic_table_region_data_validity_lt_zero(self):
        dynamic_table_region = DynamicTableRegion(name="dyn_tab", description="desc", data=[-1, 0], table=self.table)
        assert check_dynamic_table_region_data_validity(dynamic_table_region) == InspectorMessage(
            message="Some elements of dyn_tab are out of range because they are less than 0.",
            importance=Importance.CRITICAL,
            check_function_name="check_dynamic_table_region_data_validity",
            object_type="DynamicTableRegion",
            object_name="dyn_tab",
            location="/",
        )

    def test_check_dynamic_table_region_data_validity_gt_len(self):
        dynamic_table_region = DynamicTableRegion(name="dyn_tab", description="desc", data=[0, 20], table=self.table)
        assert check_dynamic_table_region_data_validity(dynamic_table_region) == InspectorMessage(
            message=(
                "Some elements of dyn_tab are out of range because they are greater than the length of the target "
                "table. Note that data should contain indices, not ids."
            ),
            importance=Importance.CRITICAL,
            check_function_name="check_dynamic_table_region_data_validity",
            object_type="DynamicTableRegion",
            object_name="dyn_tab",
            location="/",
        )

    def test_pass_check_dynamic_table_region_data(self):
        dynamic_table_region = DynamicTableRegion(name="dyn_tab", description="desc", data=[0, 1, 2], table=self.table)
        assert check_dynamic_table_region_data_validity(dynamic_table_region) is None


def test_check_empty_table_with_data():
    table = DynamicTable(name="test_table", description="")
    table.add_column(name="test_column", description="")
    table.add_row(test_column=1)
    assert check_empty_table(table=table) is None


def test_check_empty_table_without_data():
    assert check_empty_table(table=DynamicTable(name="test_table", description="")) == InspectorMessage(
        message="This table has no data added to it.",
        importance=Importance.BEST_PRACTICE_VIOLATION,
        check_function_name="check_empty_table",
        object_type="DynamicTable",
        object_name="test_table",
        location="/",
    )


def test_check_time_interval_time_columns():
    time_intervals = TimeIntervals(name="test_table", description="desc")
    time_intervals.add_row(start_time=2.0, stop_time=3.0)
    time_intervals.add_row(start_time=1.0, stop_time=2.0)

    assert check_time_interval_time_columns(time_intervals) == InspectorMessage(
        message="['start_time', 'stop_time'] are time columns but the values are not in ascending order."
        "All times should be in seconds with respect to the session start time.",
        importance=Importance.BEST_PRACTICE_VIOLATION,
        check_function_name="check_time_interval_time_columns",
        object_type="TimeIntervals",
        object_name="test_table",
        location="/",
    )


def test_pass_check_time_interval_time_columns():
    time_intervals = TimeIntervals(name="test_table", description="desc")
    time_intervals.add_row(start_time=1.0, stop_time=2.0)
    time_intervals.add_row(start_time=2.0, stop_time=3.0)

    assert check_time_interval_time_columns(time_intervals) is None


def test_check_time_intervals_stop_after_start():
    time_intervals = TimeIntervals(name="test_table", description="desc")
    time_intervals.add_row(start_time=2.0, stop_time=1.5)
    time_intervals.add_row(start_time=3.0, stop_time=1.5)
    assert check_time_intervals_stop_after_start(time_intervals) == InspectorMessage(
        message="stop_times should be greater than start_times. Make sure the stop times are with respect to the "
        "session start time.",
        importance=Importance.BEST_PRACTICE_VIOLATION,
        check_function_name="check_time_intervals_stop_after_start",
        object_type="TimeIntervals",
        object_name="test_table",
        location="/",
    )


def test_pass_check_time_intervals_stop_after_start():
    time_intervals = TimeIntervals(name="test_table", description="desc")
    time_intervals.add_row(start_time=2.0, stop_time=2.5)
    time_intervals.add_row(start_time=3.0, stop_time=3.5)
    assert check_time_intervals_stop_after_start(time_intervals) is None


class TestCheckBinaryColumns(TestCase):
    def setUp(self):
        self.table = DynamicTable(name="test_table", description="")

    def test_non_binary_pass(self):
        self.table.add_column(name="test_col", description="")
        for x in [1.0, 2.0, 3.0]:
            self.table.add_row(test_col=x)
        assert check_column_binary_capability(table=self.table) is None

    def test_array_of_non_binary_pass(self):
        self.table.add_column(name="test_col", description="")
        for x in [[1.0, 2.0], [2.0, 3.0], [1.0, 2.0]]:
            self.table.add_row(test_col=x)
        assert check_column_binary_capability(table=self.table) is None

    def test_jagged_array_of_non_binary_pass(self):
        self.table.add_column(name="test_col", description="", index=True)
        for x in [[1.0, 2.0], [1.0, 2.0, 3.0], [1.0, 2.0]]:
            self.table.add_row(test_col=x)
        assert check_column_binary_capability(table=self.table) is None

    def test_no_saved_bytes_pass(self):
        self.table.add_column(name="test_col", description="")
        for x in np.array([1, 0, 1, 0], dtype="uint8"):
            self.table.add_row(test_col=x)
        assert check_column_binary_capability(table=self.table) is None

    def test_binary_floats_fail(self):
        self.table.add_column(name="test_col", description="")
        for x in [1.0, 0.0, 1.0, 0.0, 1.0]:
            self.table.add_row(test_col=x)
        assert check_column_binary_capability(table=self.table) == [
            InspectorMessage(
                message=(
                    "test_col uses floats but has binary values [0. 1.]. Consider making it boolean instead and "
                    "renaming the column to start with 'is_'; doing so will save 35.00B."
                ),
                importance=Importance.BEST_PRACTICE_SUGGESTION,
                check_function_name="check_column_binary_capability",
                object_type="DynamicTable",
                object_name="test_table",
                location="/",
            )
        ]

    def test_binary_int_fail(self):
        self.table.add_column(name="test_col", description="")
        for x in [1, 0, 1, 0, 1]:
            self.table.add_row(test_col=x)
        if platform.system() == "Windows":
            platform_saved_bytes = "15.00B"
        else:
            platform_saved_bytes = "35.00B"
        assert check_column_binary_capability(table=self.table) == [
            InspectorMessage(
                message=(
                    "test_col uses integers but has binary values [0 1]. Consider making it boolean instead and "
                    f"renaming the column to start with 'is_'; doing so will save {platform_saved_bytes}."
                ),
                importance=Importance.BEST_PRACTICE_SUGGESTION,
                check_function_name="check_column_binary_capability",
                object_type="DynamicTable",
                object_name="test_table",
                location="/",
            )
        ]

    def test_binary_string_fail(self):
        self.table.add_column(name="test_col", description="")
        for x in ["YES", "NO", "NO", "YES"]:
            self.table.add_row(test_col=x)
        assert check_column_binary_capability(table=self.table) == [
            InspectorMessage(
                message=(
                    "test_col uses strings but has binary values ['NO' 'YES']. Consider making it boolean instead and "
                    "renaming the column to start with 'is_'; doing so will save 44.00B."
                ),
                importance=Importance.BEST_PRACTICE_SUGGESTION,
                check_function_name="check_column_binary_capability",
                object_type="DynamicTable",
                object_name="test_table",
                location="/",
            )
        ]

    def test_binary_string_pass(self):
        self.table.add_column(name="test_col", description="")
        for x in ["testing", "testingAgain", "MoreTesting", "testing"]:
            self.table.add_row(test_col=x)
        assert check_column_binary_capability(table=self.table) is None


def test_check_single_row_pass():
    table = DynamicTable(name="test_table", description="")
    table.add_column(name="test_column", description="")
    table.add_row(test_column=1)
    table.add_row(test_column=2)
    assert check_single_row(table=table) is None


def test_check_single_row_fail():
    table = DynamicTable(name="test_table", description="")
    table.add_column(name="test_column", description="")
    table.add_row(test_column=1)
    assert check_single_row(table=table) == InspectorMessage(
        message="This table has only a single row; it may be better represented by another data type.",
        importance=Importance.BEST_PRACTICE_SUGGESTION,
        check_function_name="check_single_row",
        object_type="DynamicTable",
        object_name="test_table",
        location="/",
    )


@pytest.mark.skip(reason="TODO")
def test_check_column_data_is_not_none():
    pass
