import copy
import json
import os
import shutil
from pathlib import Path

from nf_core.modules.modules_json import ModulesJson
from nf_core.modules.modules_repo import (
    NF_CORE_MODULES_DEFAULT_BRANCH,
    NF_CORE_MODULES_NAME,
    NF_CORE_MODULES_REMOTE,
    ModulesRepo,
)
from nf_core.modules.patch import ModulePatch


def test_get_modules_json(self):
    """Checks that the get_modules_json function returns the correct result"""
    mod_json_path = os.path.join(self.pipeline_dir, "modules.json")
    with open(mod_json_path, "r") as fh:
        try:
            mod_json_sb = json.load(fh)
        except json.JSONDecodeError as e:
            raise UserWarning(f"Unable to load JSON file '{mod_json_path}' due to error {e}")

    mod_json_obj = ModulesJson(self.pipeline_dir)
    mod_json = mod_json_obj.get_modules_json()

    # Check that the modules.json hasn't changed
    assert mod_json == mod_json_sb


def test_mod_json_update(self):
    """Checks whether the update function works properly"""
    mod_json_obj = ModulesJson(self.pipeline_dir)
    # Update the modules.json file
    mod_repo_obj = ModulesRepo()
    mod_json_obj.update("modules", mod_repo_obj, "MODULE_NAME", "GIT_SHA", "modules", write_file=False)
    mod_json = mod_json_obj.get_modules_json()
    assert "MODULE_NAME" in mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"]
    assert "git_sha" in mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"]["MODULE_NAME"]
    assert "GIT_SHA" == mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"]["MODULE_NAME"]["git_sha"]
    assert (
        NF_CORE_MODULES_DEFAULT_BRANCH
        == mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"]["MODULE_NAME"]["branch"]
    )
    assert "modules" in mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"]["MODULE_NAME"]["installed_by"]


def test_mod_json_create(self):
    """Test creating a modules.json file from scratch"""
    mod_json_path = os.path.join(self.pipeline_dir, "modules.json")
    # Remove the existing modules.json file
    os.remove(mod_json_path)

    # Create the new modules.json file
    # (There are no prompts as long as there are only nf-core modules)
    ModulesJson(self.pipeline_dir).create()

    # Check that the file exists
    assert os.path.exists(mod_json_path)

    # Get the contents of the file
    mod_json_obj = ModulesJson(self.pipeline_dir)
    mod_json = mod_json_obj.get_modules_json()

    mods = ["fastqc", "multiqc"]
    for mod in mods:
        assert mod in mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"]
        assert "git_sha" in mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"][mod]
        assert "branch" in mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"][mod]


def modify_main_nf(path):
    """Modify a file to test patch creation"""
    with open(path, "r") as fh:
        lines = fh.readlines()
    # Modify $meta.id to $meta.single_end
    lines[1] = '    tag "$meta.single_end"\n'
    with open(path, "w") as fh:
        fh.writelines(lines)


def test_mod_json_create_with_patch(self):
    """Test creating a modules.json file from scratch when there are patched modules"""
    mod_json_path = Path(self.pipeline_dir, "modules.json")

    # Modify the module
    module_path = Path(self.pipeline_dir, "modules", "nf-core", "fastqc")
    modify_main_nf(module_path / "main.nf")

    # Try creating a patch file
    patch_obj = ModulePatch(self.pipeline_dir, NF_CORE_MODULES_REMOTE, NF_CORE_MODULES_DEFAULT_BRANCH)
    patch_obj.patch("fastqc")

    # Remove the existing modules.json file
    os.remove(mod_json_path)

    # Create the new modules.json file
    ModulesJson(self.pipeline_dir).create()

    # Check that the file exists
    assert mod_json_path.is_file()

    # Get the contents of the file
    mod_json_obj = ModulesJson(self.pipeline_dir)
    mod_json = mod_json_obj.get_modules_json()

    # Check that fastqc is in the file
    assert "fastqc" in mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"]
    assert "git_sha" in mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"]["fastqc"]
    assert "branch" in mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"]["fastqc"]

    # Check that fastqc/main.nf maintains the changes
    with open(module_path / "main.nf", "r") as fh:
        lines = fh.readlines()
    assert lines[1] == '    tag "$meta.single_end"\n'


def test_mod_json_up_to_date(self):
    """
    Checks if the modules.json file is up to date
    when no changes have been made to the pipeline
    """
    mod_json_obj = ModulesJson(self.pipeline_dir)
    mod_json_before = mod_json_obj.get_modules_json()
    mod_json_obj.check_up_to_date()
    mod_json_after = mod_json_obj.get_modules_json()

    # Check that the modules.json hasn't changed
    assert mod_json_before == mod_json_after


def test_mod_json_up_to_date_module_removed(self):
    """
    Reinstall a module that has an entry in the modules.json
    but is missing in the pipeline
    """
    # Remove the fastqc module
    fastqc_path = os.path.join(self.pipeline_dir, "modules", NF_CORE_MODULES_NAME, "fastqc")
    shutil.rmtree(fastqc_path)

    # Check that the modules.json file is up to date, and reinstall the module
    mod_json_obj = ModulesJson(self.pipeline_dir)
    mod_json_obj.check_up_to_date()

    # Check that the module has been reinstalled
    files = ["main.nf", "meta.yml"]
    assert os.path.exists(fastqc_path)
    for f in files:
        assert os.path.exists(os.path.join(fastqc_path, f))


def test_mod_json_up_to_date_reinstall_fails(self):
    """
    Try reinstalling a module where the git_sha is invalid
    """
    mod_json_obj = ModulesJson(self.pipeline_dir)

    # Update the fastqc module entry to an invalid git_sha
    mod_json_obj.update("modules", ModulesRepo(), "fastqc", "INVALID_GIT_SHA", "modules", write_file=True)

    # Remove the fastqc module
    fastqc_path = os.path.join(self.pipeline_dir, "modules", NF_CORE_MODULES_NAME, "fastqc")
    shutil.rmtree(fastqc_path)

    # Check that the modules.json file is up to date, and remove the fastqc module entry
    mod_json_obj.check_up_to_date()
    mod_json = mod_json_obj.get_modules_json()

    # Check that the module has been removed from the modules.json
    assert "fastqc" not in mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"]


def test_mod_json_repo_present(self):
    """Tests the repo_present function"""
    mod_json_obj = ModulesJson(self.pipeline_dir)

    assert mod_json_obj.repo_present(NF_CORE_MODULES_REMOTE) is True
    assert mod_json_obj.repo_present("INVALID_REPO") is False


def test_mod_json_module_present(self):
    """Tests the module_present function"""
    mod_json_obj = ModulesJson(self.pipeline_dir)

    assert mod_json_obj.module_present("fastqc", NF_CORE_MODULES_REMOTE, NF_CORE_MODULES_NAME) is True
    assert mod_json_obj.module_present("INVALID_MODULE", NF_CORE_MODULES_REMOTE, NF_CORE_MODULES_NAME) is False
    assert mod_json_obj.module_present("fastqc", "INVALID_REPO", "INVALID_DIR") is False
    assert mod_json_obj.module_present("INVALID_MODULE", "INVALID_REPO", "INVALID_DIR") is False


def test_mod_json_get_module_version(self):
    """Test the get_module_version function"""
    mod_json_obj = ModulesJson(self.pipeline_dir)
    mod_json = mod_json_obj.get_modules_json()
    assert (
        mod_json_obj.get_module_version("fastqc", NF_CORE_MODULES_REMOTE, NF_CORE_MODULES_NAME)
        == mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"]["nf-core"]["fastqc"]["git_sha"]
    )
    assert mod_json_obj.get_module_version("INVALID_MODULE", NF_CORE_MODULES_REMOTE, NF_CORE_MODULES_NAME) is None


def test_mod_json_dump(self):
    """Tests the dump function"""
    mod_json_obj = ModulesJson(self.pipeline_dir)
    mod_json = mod_json_obj.get_modules_json()
    # Remove the modules.json file
    mod_json_path = os.path.join(self.pipeline_dir, "modules.json")
    os.remove(mod_json_path)

    # Check that the dump function creates the file
    mod_json_obj.dump()
    assert os.path.exists(mod_json_path)

    # Check that the dump function writes the correct content
    with open(mod_json_path, "r") as f:
        try:
            mod_json_new = json.load(f)
        except json.JSONDecodeError as e:
            raise UserWarning(f"Unable to load JSON file '{mod_json_path}' due to error {e}")
    assert mod_json == mod_json_new


def test_mod_json_with_empty_modules_value(self):
    # Load module.json and remove the modules entry
    mod_json_obj = ModulesJson(self.pipeline_dir)
    mod_json_obj.create()  # Create modules.json explicitly to get correct module sha
    mod_json_orig = mod_json_obj.get_modules_json()
    mod_json = copy.deepcopy(mod_json_orig)
    mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"] = ""
    # save the altered module.json and load it again to check if it will fix itself
    mod_json_obj.modules_json = mod_json
    mod_json_obj.dump()
    mod_json_obj_new = ModulesJson(self.pipeline_dir)
    mod_json_obj_new.check_up_to_date()
    mod_json_new = mod_json_obj_new.get_modules_json()
    assert mod_json_orig == mod_json_new


def test_mod_json_with_missing_modules_entry(self):
    # Load module.json and remove the modules entry
    mod_json_obj = ModulesJson(self.pipeline_dir)
    mod_json_obj.create()  # Create modules.json explicitly to get correct module sha
    mod_json_orig = mod_json_obj.get_modules_json()
    mod_json = copy.deepcopy(mod_json_orig)
    mod_json["repos"][NF_CORE_MODULES_REMOTE].pop("modules")
    # save the altered module.json and load it again to check if it will fix itself
    mod_json_obj.modules_json = mod_json
    mod_json_obj.dump()
    mod_json_obj_new = ModulesJson(self.pipeline_dir)
    mod_json_obj_new.check_up_to_date()
    mod_json_new = mod_json_obj_new.get_modules_json()
    assert mod_json_orig == mod_json_new
