# encoding: utf-8
#
# This file is a part of the LinkAhead Project.
#
# Copyright (C) 2024 Indiscale GmbH <info@indiscale.com>
# Copyright (C) 2024 Daniel Hornung <d.hornung@indiscale.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Testing the conversion from XLSX to JSON"""


import json
import os
import re
import tempfile

from types import SimpleNamespace

import jsonschema.exceptions as schema_exc
import pytest
import caosadvancedtools.table_json_conversion.convert as convert
from openpyxl import load_workbook

from .utils import compare_workbooks


def rfp(*pathcomponents):
    """Return full path, a shorthand convenience function.
    """
    return os.path.join(os.path.dirname(__file__), *pathcomponents)


def fill_and_compare(json_file: str, template_file: str, known_good: str,
                     schema: str = None, custom_output: str = None):
    """Fill the data into a template and compare to a known good.

Parameters:
-----------
schema: str, optional,
  Json schema to validate against.
custom_output: str, optional
  If given, write to this file and drop into an IPython shell.  For development only.
    """
    with tempfile.TemporaryDirectory() as tmpdir:
        outfile = os.path.join(tmpdir, 'test.xlsx')
        assert not os.path.exists(outfile)
        if custom_output is not None:
            outfile = custom_output
        # fill_template(data=json_file, template=template_file, result=outfile,
        #               validation_schema=schema)
        assert os.path.exists(outfile)
        generated = load_workbook(outfile)  # workbook can be read
    known_good_wb = load_workbook(known_good)
    compare_workbooks(generated, known_good_wb)


def _assert_equal_jsons(json1, json2, allow_none: bool = True, allow_empty: bool = True,
                        path: list = None) -> None:
    """Compare two json objects for near equality.

Raise an assertion exception if they are not equal."""
    if path is None:
        path = []
    assert isinstance(json1, dict) == isinstance(json2, dict), f"Type mismatch, path: {path}"
    if isinstance(json1, dict):
        keys = set(json1.keys()).union(json2.keys())
        for key in keys:
            this_path = path + [key]
            # Case 1: both exist
            if key in json1 and key in json2:
                el1 = json1[key]
                el2 = json2[key]
                assert type(el1) is type(el2), f"Type mismatch, path: {this_path}"
                if isinstance(el1, (dict, list)):
                    _assert_equal_jsons(el1, el2, allow_none=allow_none, allow_empty=allow_empty,
                                        path=this_path)
                assert el1 == el2, f"Values at path {this_path} are not equal:\n{el1},\n{el2}"
                continue
            # Case 2: only one exists
            existing = json1.get(key, json2.get(key))
            assert (allow_none and existing is None) or (allow_empty and existing == []), (
                f"Element at path {this_path} is None or empty in one json and does not exist in "
                "the other.")

    assert isinstance(json1, list) and isinstance(json2, list), f"Type mismatch, path: {path}"
    assert len(json1) == len(json2), f"Lists must have equal length, path: {path}"
    for idx, (el1, el2) in enumerate(zip(json1, json2)):
        this_path = path + [idx]
        assert isinstance(el1, dict) and isinstance(el2, dict), (
            f"List elements must be dicts: path: {this_path}")
        _assert_equal_jsons(el1, el2, allow_none=allow_none, allow_empty=allow_empty,
                            path=this_path)


def test_conversions():
    result = convert.to_dict(xlsx=rfp("data/simple_data.xlsx"), schema=rfp("data/simple_schema.json"))
    expected = json.load(open(rfp("data/simple_data.json")))
    # result = convert.to_dict(xlsx=rfp("data/multiple_refs_data.xlsx"),
    #                          schema=rfp("data/multiple_refs_schema.json"))
    # expected = json.load(open(rfp("data/multiple_refs_data.json")))
    breakpoint()
    _assert_equal_jsons(result, expected)
    breakpoint()
    # conv = XLSXConverter(schema=rfp("data/simple_schema.json"))
    # result = conv.to_dict(rfp("data/simple_template.xlsx"))


def test_set_in_nested():
    set_in_nested = convert._set_in_nested  # pylint: disable=protected-access

    test_data_in = [
        {"mydict": {}, "path": ["a", 1], "value": 3},
        {"mydict": {"a": 1}, "path": ["a"], "value": 3, "overwrite": True},
        {"mydict": {"a": 1}, "path": ["a", 1], "value": 3, "overwrite": True},
        {"mydict": {"b": 2}, "path": ["a", 1, 3.141], "value": 3},
        {"mydict": {}, "path": ["X", "Y", "a", 1], "value": 3, "prefix": ["X", "Y"]},
    ]
    test_data_out = [
        {"a": {1: 3}},
        {"a": 3},
        {"a": {1: 3}},
        {"a": {1: {3.141: 3}}, "b": 2},
        {"a": {1: 3}},
    ]

    for data_in, data_out in zip(test_data_in, test_data_out):
        assert set_in_nested(**data_in) == data_out

    # Testing exceptions
    test_data_in = [
        {"mydict": {"a": 1}, "path": ["a"], "value": 3},
        {"mydict": {"a": 1}, "path": ["a", 1], "value": 3},
        {"mydict": {}, "path": ["a", 1], "value": 3, "prefix": ["X", "Y", "Z"]},
    ]
    exceptions = [
        [ValueError, r"There is already some value at \[a\]"],
        [ValueError, r"There is already some value at \[1\]"],
        [KeyError, r"Path does not start with prefix: \['X', 'Y', 'Z'\] not in \['a', 1\]"],
    ]

    for data_in, (exc_out, match) in zip(test_data_in, exceptions):
        with pytest.raises(exc_out, match=match):
            set_in_nested(**data_in)


def test_group_foreign_paths():
    group = convert._group_foreign_paths  # pylint: disable=protected-access

    foreign = [
        ["A", "x", 1.1],
        ["A", "y", "z", "some text"],
        ["A", "B", "CC", "x", 42],
    ]
    common = ["A", "B", "CC"]
    common_wrong = ["A", "B", "C"]
    expected = [
        SimpleNamespace(stringpath="A", path=["A"], subpath=["A"],
                        definitions=[["x", 1.1], ["y", "z", "some text"]]),
        SimpleNamespace(stringpath="A.B.CC", path=["A", "B", "CC"], subpath=["B", "CC"],
                        definitions=[["x", 42]]),
    ]

    with pytest.raises(ValueError, match=re.escape(
            "Foreign keys must cover the complete `common` depth.")):
        result = group(foreign=foreign, common=common_wrong)
    result = group(foreign=foreign, common=common)
    assert result == expected