import pytest
import sys
import os
import json
from primrose.configuration.configuration import Configuration
from primrose.writers.abstract_file_writer import AbstractFileWriter
from primrose.node_factory import NodeFactory


@pytest.fixture
def mock_env(monkeypatch):
    monkeypatch.setenv("PRIMROSE_EXT_NODE_PACKAGE", "test")


def test_init_error0():
    config = {}
    with pytest.raises(Exception) as e:
        Configuration(config_location=None, is_dict_config=True, dict_config=config)
    assert "Did not find required top-level key implementation_config" in str(e)


def test_init_error1():
    with pytest.raises(Exception) as e:
        Configuration(None)
    if sys.version_info[:2] == (3, 5):
        assert "stat: can't specify None for path argument" in str(e)
    else:
        assert (
            "stat: path should be string, bytes, os.PathLike or integer, not NoneType"
            in str(e)
        )


def test_init_error2():
    with pytest.raises(Exception) as e:
        Configuration(config_location="junky_path_does_not_exist")
    assert "config file at: junky_path_does_not_exist not found" in str(e)


def test_init_error3():
    with pytest.raises(Exception) as e:
        Configuration(config_location=None, is_dict_config=True, dict_config=None)
    assert "expected dict_config was None" in str(e)


def test_init_error4():
    with pytest.raises(Exception) as e:
        Configuration(config_location=None, is_dict_config=True, dict_config="1234")
    assert "did not receive expected dict_config" in str(e)


def test_init_error5():
    config = {"implementation_config": {"k1": {"a": 1}, "k2": {"a": 2}}}
    with pytest.raises(Exception) as e:
        Configuration(config_location=None, is_dict_config=True, dict_config=config)
    assert "Operations must all have unique names in the configuration" in str(e)


def test_init_error6():
    with pytest.raises(Exception) as e:
        filename = "test/test_dupe_toplevel.json"
        Configuration(config_location=filename, is_dict_config=False, dict_config=None)
    assert "duplicate key: 'model_config'" in str(e)


def test_init_error7():
    config = {"junk": {}}
    with pytest.raises(Exception) as e:
        Configuration(config_location=None, is_dict_config=True, dict_config=config)
    assert (
        "Unsupported top-level key: junk. Supported keys are ['metadata', 'implementation_config']"
        in str(e)
    )


def test_init_error8():
    with pytest.raises(ValueError) as e:
        Configuration("test/tennis.csv")
    assert (
        "config file at: test/tennis.csv has improper extension type - please use a .json or .yml file"
        in str(e)
    )


def test_metadata():
    config = {"metadata": {"k": "v"}, "implementation_config": {}}
    c = Configuration(config_location=None, is_dict_config=True, dict_config=config)
    assert c.config_metadata["k"] == "v"


def test_init_ok():
    config = {"implementation_config": {}}
    c = Configuration(config_location=None, is_dict_config=True, dict_config=config)
    assert c.config_string
    assert c.config_hash


def test_init_ok2():
    config = {
        "metadata": {"traverser": "DepthFirstTraverser"},
        "implementation_config": {
            "reader_config": {
                "csv_reader": {
                    "class": "CsvReader",
                    "filename": "some/path/to/file",
                    "destinations": [],
                }
            }
        },
    }
    c = Configuration(config_location=None, is_dict_config=True, dict_config=config)
    assert c.config_string
    assert (
        c.config_hash
        == "b434870806fe0c61ddf2b417ae62c1be519b069fb6e12572f4ff8143f98086ff"
    )


def test_unrecognized():
    config = {
        "implementation_config": {
            "junky_config": {
                "csv_reader": {
                    "class": "CsvReader",
                    "filename": "some/path/to/file",
                    "destinations": [],
                }
            }
        }
    }
    with pytest.raises(Exception) as e:
        Configuration(config_location=None, is_dict_config=True, dict_config=config)
    assert "Unspported operation: junky_config" in str(e)


def test_nopipelinekey():
    config = {
        "implementation_config": {
            "pipeline_config": {
                "corpus_pipeline": {"destinations": ["recipe_name_model"]}
            }
        }
    }
    with pytest.raises(Exception) as e:
        Configuration(config_location=None, is_dict_config=True, dict_config=config)
    assert "No class key found in pipeline_config.corpus_pipeline" in str(e)


def test_metadata2():
    config = {"metadata": {"traverser": "doesnotexist"}, "implementation_config": {}}
    with pytest.raises(Exception) as e:
        Configuration(config_location=None, is_dict_config=True, dict_config=config)
    assert "doesnotexist is not a valid and/or registered Traverser" in str(e)


def test_check_metadata():
    config = {
        "metadata": {"data_object": {"read_from_cache": True}},
        "implementation_config": {},
    }
    with pytest.raises(Exception) as e:
        Configuration(config_location=None, is_dict_config=True, dict_config=config)
    assert (
        "metadata.data_object: if read_from_cache==true, you must set 'read_filename'"
        in str(e)
    )


def test_check_metadata2():
    config = {
        "metadata": {
            "data_object": {
                "read_from_cache": True,
                "read_filename": "path/does/not/exist",
            }
        },
        "implementation_config": {},
    }
    with pytest.raises(Exception) as e:
        Configuration(config_location=None, is_dict_config=True, dict_config=config)
    assert "Invalid metadata.data_object.read_filename: path/does/not/exist" in str(e)


def test_check_metadata3():
    config = {
        "metadata": {"data_object": {"write_to_cache": True}},
        "implementation_config": {},
    }
    with pytest.raises(Exception) as e:
        Configuration(config_location=None, is_dict_config=True, dict_config=config)
    assert (
        "metadata.data_object: if write_to_cache==true, you must set 'write_filename'"
        in str(e)
    )


def test_check_setions():
    config = {
        "metadata": {"section_registry": ["phase_1"]},
        "implementation_config": {
            "reader_config": {
                "read_data": {
                    "class": "CsvReader",
                    "filename": "test/tennis.csv",
                    "destinations": [],
                }
            }
        },
    }
    with pytest.raises(Exception) as e:
        Configuration(config_location=None, is_dict_config=True, dict_config=config)
    assert (
        "Following sections from metadata were not found implementation: {'phase_1'}"
        in str(e)
    )


def test_check_setions2():
    config = {
        "metadata": {"section_registry": ["reader_config"]},
        "implementation_config": {
            "reader_config": {
                "read_data": {
                    "class": "CsvReader",
                    "filename": "test/tennis.csv",
                    "destinations": [],
                }
            },
            "writer_config": {},
        },
    }
    with pytest.raises(Exception) as e:
        Configuration(config_location=None, is_dict_config=True, dict_config=config)
    assert (
        "Following sections from implementation were not found in metadata: {'writer_config'}"
        in str(e)
    )


def test_check_setions3():
    config = {
        "metadata": {"section_registry": ["reader_config", "writer_config"]},
        "implementation_config": {
            "reader_config": {
                "read_data": {
                    "class": "CsvReader",
                    "filename": "test/tennis.csv",
                    "destinations": [],
                }
            },
            "writer_config": {},
        },
    }
    # this should not raise an exception
    Configuration(config_location=None, is_dict_config=True, dict_config=config)


def test_check_setions4():
    config = {
        "metadata": {"section_registry": ["phase1", "phase2"]},
        "implementation_config": {
            "phase1": {
                "read_data": {
                    "class": "CsvReader",
                    "filename": "test/tennis.csv",
                    "destinations": [],
                }
            },
            "phase2": {},
        },
    }
    # this should not raise an exception
    Configuration(config_location=None, is_dict_config=True, dict_config=config)


def test_sections_in_order1():
    config = {
        "metadata": {"section_registry": ["phase1", "phase2"]},
        "implementation_config": {
            "phase1": {
                "read_data": {
                    "class": "CsvReader",
                    "filename": "test/tennis.csv",
                    "destinations": [],
                }
            },
            "phase2": {},
        },
    }
    config = Configuration(
        config_location=None, is_dict_config=True, dict_config=config
    )
    sections, source = config.sections_in_order()
    assert sections == ["phase1", "phase2"]
    assert source == "section_registry"


def test_sections_in_order2():
    config = {
        "metadata": {},
        "implementation_config": {
            "reader_config": {
                "read_data": {
                    "class": "CsvReader",
                    "filename": "test/tennis.csv",
                    "destinations": [],
                }
            },
            "writer_config": {},
        },
    }
    config = Configuration(
        config_location=None, is_dict_config=True, dict_config=config
    )
    sections, source = config.sections_in_order()
    assert sections == ["reader_config", "writer_config"]
    assert source == "default"


def test_sections_in_order3():
    config = {
        "metadata": {"section_run": ["writer_config"]},
        "implementation_config": {
            "reader_config": {
                "read_data": {
                    "class": "CsvReader",
                    "filename": "test/tennis.csv",
                    "destinations": ["recipe_csv_writer"],
                }
            },
            "writer_config": {
                "recipe_csv_writer": {
                    "class": "CsvWriter",
                    "key": "test_data",
                    "dir": "cache",
                    "filename": "unittest_similar_recipes.csv",
                }
            },
        },
    }
    config = Configuration(
        config_location=None, is_dict_config=True, dict_config=config
    )
    sections, source = config.sections_in_order()
    assert sections == ["writer_config"]
    assert source == "section_run"


def test_registration():
    class TestWriterUnreg(AbstractFileWriter):
        def __init__(self, configuration, instance_name):
            pass

        def write(self, data_dict):
            pass

    config = {
        "implementation_config": {
            "reader_config": {
                "read_data": {"class": "TestWriterUnreg", "destinations": []}
            }
        }
    }
    with pytest.raises(Exception) as e:
        Configuration(config_location=None, is_dict_config=True, dict_config=config)
    assert "Cannot register node class TestWriterUnreg" in str(e)


def test_comments_in_json():
    # should not raise exception even though there are comments in the JSON
    config = Configuration(config_location="test/config_with_comments.json")
    assert list(config.config.keys()) == ["reader_config", "writer_config"]
    assert config.config["reader_config"]["read_data"]["class"] == "CsvReader"
    assert list(config.config["reader_config"]["read_data"]["destinations"]) == [
        "write_output"
    ]
    # "{'reader_config': {'read_data': {'class': 'CsvReader', 'filename': 'data/tennis.csv', 'destinations': ['write_output']}}, 'writer_config': {'write_output': {'class': 'CsvWriter', 'key': 'data', 'dir': 'cache', 'filename': 'tennis_output.csv'}}}"


def test_perform_any_config_fragment_substitution_bad():
    config_str = """
    {
        {% include "does/not/exist" %}
        "implementation_config": {
        }
    }
    """
    with pytest.raises(Exception) as e:
        Configuration.perform_any_config_fragment_substitution(config_str)
    assert "Substitution files do not exist: does/not/exist" in str(e)


def test_perform_any_config_fragment_substitution_default():
    config_str = """
    {
        {% include "test/metadata_fragment.json" %}
        "implementation_config": {
            {% include "test/read_write_fragment.json" %}
        }
    }
    """
    final_str = Configuration.perform_any_config_fragment_substitution(config_str)
    expected = """
    {
        "metadata":{
            "test": "default"
        },
        "implementation_config": {

        "reader_config": {
            "read_data": {
                "class": "CsvReader",
                "filename": "data/tennis.csv",
                "destinations": [
                    "write_output"
                ]
            }
        },
        "writer_config": {
            "write_output": {
                "class": "CsvWriter",
                "key": "data",
                "dir": "cache",
                "filename": "tennis_output.csv"
            }
        }

        }
    }
    """
    assert json.loads(final_str) == json.loads(expected)

def test_perform_any_config_fragment_substitution_env_var(monkeypatch):
    monkeypatch.setenv("TEST","foo")
    config_str = """
    {
        {% include "test/metadata_fragment.json" %}
        "implementation_config": {
            {% include "test/read_write_fragment.json" %}
        }
    }
    """
    final_str = Configuration.perform_any_config_fragment_substitution(config_str)
    expected = """
    {
        "metadata":{
            "test": "foo"
        },
        "implementation_config": {

        "reader_config": {
            "read_data": {
                "class": "CsvReader",
                "filename": "data/tennis.csv",
                "destinations": [
                    "write_output"
                ]
            }
        },
        "writer_config": {
            "write_output": {
                "class": "CsvWriter",
                "key": "data",
                "dir": "cache",
                "filename": "tennis_output.csv"
            }
        }

        }
    }
    """
    assert json.loads(final_str) == json.loads(expected)

def test_yaml_config1():
    config_yaml = Configuration(config_location="test/hello_world_tennis.yml")
    config_json = Configuration(config_location="test/hello_world_tennis.json")
    assert config_yaml.config_hash == config_json.config_hash


def test_yaml_config2():
    c = Configuration("test/config_substitution.yml")
    assert c.config_string
    assert c.config_hash


def test_yaml_perform_any_config_fragment_substitution_default():
    config_str = """
{% include "test/metadata_fragment.yml" %}
implementation_config:
{% include "test/read_write_fragment.yml" %}
    """
    final_str = Configuration.perform_any_config_fragment_substitution(config_str)
    expected = """
metadata:
  test: default
implementation_config:
  reader_config:
    read_data:
      class: CsvReader
      destinations:
      - write_output
      filename: data/tennis.csv
  writer_config:
    write_output:
      class: CsvWriter
      dir: cache
      filename: tennis_output.csv
      key: data
    """
    assert final_str == expected

def test_yaml_perform_any_config_fragment_substitution_env_var(monkeypatch):
    monkeypatch.setenv("TEST","foo")
    config_str = """
{% include "test/metadata_fragment.yml" %}
implementation_config:
{% include "test/read_write_fragment.yml" %}
    """
    final_str = Configuration.perform_any_config_fragment_substitution(config_str)
    expected = """
metadata:
  test: foo
implementation_config:
  reader_config:
    read_data:
      class: CsvReader
      destinations:
      - write_output
      filename: data/tennis.csv
  writer_config:
    write_output:
      class: CsvWriter
      dir: cache
      filename: tennis_output.csv
      key: data
    """
    assert final_str == expected


def test_class_prefix(mock_env):
    config_path = {
        "implementation_config": {
            "reader_config": {
                "read_data": {
                    "class": "TestExtNode",
                    "class_prefix": "test/ext_node_example.py",
                    "destinations": [],
                }
            }
        }
    }
    config_dot = {
        "implementation_config": {
            "reader_config": {
                "read_data": {
                    "class": "TestExtNode",
                    "class_prefix": "test.ext_node_example",
                    "destinations": [],
                }
            }
        }
    }
    for config in [config_path, config_dot]:
        config = Configuration(
            config_location=None, is_dict_config=True, dict_config=config
        )
        assert config.config_string
        assert config.config_hash
        NodeFactory().unregister("TestExtNode")


def test_class_package(mock_env):
    config_path = {
        "metadata": {"class_package": "test"},
        "implementation_config": {
            "reader_config": {"read_data": {"class": "TestExtNode", "destinations": []}}
        },
    }
    config_full_path = {
        "metadata": {"class_package": "test/ext_node_example.py"},
        "implementation_config": {
            "reader_config": {"read_data": {"class": "TestExtNode", "destinations": []}}
        },
    }
    config_full_dot = {
        "metadata": {"class_package": "test"},
        "implementation_config": {
            "reader_config": {
                "read_data": {
                    "class": "TestExtNode",
                    "class_prefix": "ext_node_example",
                    "destinations": [],
                }
            }
        },
    }
    for config in [config_full_path, config_path, config_full_dot]:
        config = Configuration(
            config_location=None, is_dict_config=True, dict_config=config
        )
        assert config.config_string
        assert config.config_hash
        NodeFactory().unregister("TestExtNode")


def test_env_override_class_package(mock_env):
    config = {
        "metadata": {"class_package": "junk"},
        "implementation_config": {
            "reader_config": {"read_data": {"class": "TestExtNode", "destinations": []}}
        },
    }
    config = Configuration(
        config_location=None, is_dict_config=True, dict_config=config
    )
    assert config.config_string
    assert config.config_hash
    NodeFactory().unregister("TestExtNode")


def test_incorrect_class_package():
    config = {
        "metadata": {"class_package": "junk"},
        "implementation_config": {
            "reader_config": {"read_data": {"class": "TestExtNode", "destinations": []}}
        },
    }
    with pytest.raises(Exception) as e:
        config = Configuration(
            config_location=None, is_dict_config=True, dict_config=config
        )
    assert "Cannot register node class TestExtNode" in str(e)


def test_incorrect_class_package2():
    config = {
        "metadata": {"class_package": "test"},
        "implementation_config": {
            "reader_config": {
                "read_data": {
                    "class": "TestExtNode",
                    "class_prefix": "junk",
                    "destinations": [],
                }
            }
        },
    }
    with pytest.raises(Exception) as e:
        config = Configuration(
            config_location=None, is_dict_config=True, dict_config=config
        )
    assert "Cannot register node class TestExtNode" in str(e)
