import builtins
import io
import json
import os
import shutil
import sys
import unittest
from contextlib import contextmanager, redirect_stdout
from copy import deepcopy

import pytest
import requests
from kinto_http import Client, exceptions
from ruamel.yaml import YAML

from kinto_wizard.__main__ import main


def load(server, auth, file, bucket=None, collection=None, extra=None):
    cmd = "kinto-wizard {} --server={} --auth={}"

    if bucket:
        cmd += " --bucket={}".format(bucket)

    if collection:
        cmd += " --collection={}".format(collection)

    if extra:
        cmd += " " + extra

    load_cmd = cmd.format("load {}".format(file), server, auth)
    sys.argv = load_cmd.strip().split(" ")
    return main()


def dump(server, auth, bucket=None, collection=None, extra=None):
    cmd = "kinto-wizard {} --server={} --auth={}"
    dump_cmd = cmd.format("dump --full", server, auth)

    if bucket:
        dump_cmd += " --bucket={}".format(bucket)

    if collection:
        dump_cmd += " --collection={}".format(collection)

    if extra:
        dump_cmd += " " + extra

    sys.argv = dump_cmd.split(" ")
    output = io.StringIO()
    with redirect_stdout(output):
        main()
    output.flush()

    # Check that identical to original file.
    return output.getvalue()


def validate(filename):
    sys.argv = ["kinto-wizard", "validate", filename]
    return main()


def assert_identical(a, b):
    yaml = YAML(typ="safe")
    a_parsed = yaml.load(a)
    b_parsed = yaml.load(b)
    assert a_parsed == b_parsed


class FunctionalTest(unittest.TestCase):
    server = os.getenv("SERVER_URL", "http://localhost:8888/v1")
    auth = os.getenv("AUTH", "user:pass")
    file = os.getenv("FILE", "tests/kinto.yaml")

    def setUp(self):
        requests.post(self.server + "/__flush__")

    def load(self, bucket=None, collection=None, filename=None, extra=None):
        return load(self.server, self.auth, filename or self.file, bucket, collection, extra)

    def dump(self, bucket=None, collection=None, extra=None):
        return dump(self.server, self.auth, bucket, collection, extra)

    def validate(self, filename=None, code=0):
        try:
            validate(filename or self.file)
        except SystemExit as e:
            if e.code == code:
                return
            else:
                self.fail(f"Unexpected validation status {e.code} != {code}")


class DryRunLoad(FunctionalTest):
    def test_dry_round_trip(self):
        cmd = "kinto-wizard {} --server={} --auth={} --dry-run"
        load_cmd = cmd.format("load {}".format(self.file), self.server, self.auth)
        sys.argv = load_cmd.split(" ")
        main()
        client = Client(server_url=self.server, auth=tuple(self.auth.split(":")))
        with pytest.raises(exceptions.KintoException):
            client.get_bucket(id="staging")


@contextmanager
def mockInput(mock):
    original_input = builtins.input
    builtins.input = lambda _: mock
    yield
    builtins.input = original_input


class SimpleDump(FunctionalTest):
    def test_round_trip(self):
        cmd = "kinto-wizard {} --server={} --auth={}"
        load_cmd = cmd.format("load {}".format(self.file), self.server, self.auth)
        sys.argv = load_cmd.split(" ")
        main()

        dump_cmd = (
            cmd.format("dump", self.server, self.auth) + " --permissions --collections --groups"
        )
        sys.argv = dump_cmd.split(" ")
        output = io.StringIO()
        with redirect_stdout(output):
            main()
        output.flush()

        # Check that identical to original file.
        generated = output.getvalue()
        with open(self.file) as f:
            assert_identical(f.read(), generated)


class FullDump(FunctionalTest):
    file = os.getenv("FILE", "tests/kinto-full.yaml")

    def test_round_trip(self):
        # Load some data
        cmd = "kinto-wizard {} --server={} --auth={}"
        load_cmd = cmd.format("load {}".format(self.file), self.server, self.auth)
        sys.argv = load_cmd.split(" ")
        main()

        cmd = "kinto-wizard {} --server={} --auth={} --full"
        load_cmd = cmd.format("dump", self.server, self.auth)
        sys.argv = load_cmd.split(" ")
        output = io.StringIO()
        with redirect_stdout(output):
            main()
        output.flush()

        # Check that identical to original file.
        generated = output.getvalue()
        with open(self.file) as f:
            assert_identical(f.read(), generated)

    def test_round_trip_with_client_wins(self):
        # Load some data
        cmd = "kinto-wizard {} --server={} --auth={}"
        load_cmd = cmd.format("load {}".format(self.file), self.server, self.auth)
        sys.argv = load_cmd.split(" ")
        main()

        # Change something that could make the server to fail.
        client = Client(server_url=self.server, auth=tuple(self.auth.split(":")))
        client.update_record(
            bucket="build-hub",
            collection="archives",
            id="0831d549-0a69-48dd-b240-feef94688d47",
            data={},
        )
        record = client.get_record(
            bucket="build-hub", collection="archives", id="0831d549-0a69-48dd-b240-feef94688d47"
        )
        assert set(record["data"].keys()) == {"id", "last_modified"}
        cmd = "kinto-wizard {} --server={} -D --auth={} --force"
        load_cmd = cmd.format("load {}".format(self.file), self.server, self.auth)
        sys.argv = load_cmd.split(" ")
        main()
        record = client.get_record(
            bucket="build-hub", collection="archives", id="0831d549-0a69-48dd-b240-feef94688d47"
        )
        assert set(record["data"].keys()) != {"id", "last_modified"}

    def test_round_trip_with_client_wins_and_delete_missing_records(self):
        # Load some data
        cmd = "kinto-wizard {} --server={} --auth={}"
        load_cmd = cmd.format("load {}".format(self.file), self.server, self.auth)
        sys.argv = load_cmd.split(" ")
        main()

        # Change something that could make the server to fail.
        client = Client(server_url=self.server, auth=tuple(self.auth.split(":")))
        client.create_record(
            bucket="build-hub",
            collection="archives",
            id="8031d549-0a69-48dd-b240-feef94688d47",
            data={},
        )
        cmd = "kinto-wizard {} --server={} -D --auth={} --force --delete-records"
        load_cmd = cmd.format("load {}".format(self.file), self.server, self.auth)
        sys.argv = load_cmd.split(" ")
        main()
        with pytest.raises(exceptions.KintoException) as exc:
            client.get_record(
                bucket="build-hub",
                collection="archives",
                id="8031d549-0a69-48dd-b240-feef94688d47",
            )
        assert "'Not Found'" in str(exc.value)

    def test_round_trip_with_delete_missing_records_ask_for_confirmation(self):
        # Load some data
        cmd = "kinto-wizard {} --server={} --auth={}"
        load_cmd = cmd.format("load {}".format(self.file), self.server, self.auth)
        sys.argv = load_cmd.split(" ")
        main()

        # Change something that could make the server to fail.
        client = Client(server_url=self.server, auth=tuple(self.auth.split(":")))
        client.create_record(
            bucket="build-hub",
            collection="archives",
            id="8031d549-0a69-48dd-b240-feef94688d47",
            data={},
        )
        cmd = "kinto-wizard {} --server={} -D --auth={} --delete-records"
        load_cmd = cmd.format("load {}".format(self.file), self.server, self.auth)
        sys.argv = load_cmd.split(" ")

        with mockInput("yes"):
            main()

        with pytest.raises(exceptions.KintoException) as exc:
            client.get_record(
                bucket="build-hub",
                collection="archives",
                id="8031d549-0a69-48dd-b240-feef94688d47",
            )
        assert "'Not Found'" in str(exc.value)

    def test_round_trip_with_delete_missing_records_handle_misconfirmation(self):
        # Load some data
        cmd = "kinto-wizard {} --server={} --auth={}"
        load_cmd = cmd.format("load {}".format(self.file), self.server, self.auth)
        sys.argv = load_cmd.split(" ")
        main()

        # Change something that could make the server to fail.
        client = Client(server_url=self.server, auth=tuple(self.auth.split(":")))
        client.create_record(
            bucket="build-hub",
            collection="archives",
            id="8031d549-0a69-48dd-b240-feef94688d47",
            data={},
        )
        cmd = "kinto-wizard {} --server={} -D --auth={} --delete-records"
        load_cmd = cmd.format("load {}".format(self.file), self.server, self.auth)
        sys.argv = load_cmd.split(" ")

        with mockInput("no"):
            with pytest.raises(SystemExit):
                main()


class DataRecordsDump(FunctionalTest):
    file = os.getenv("FILE", "tests/kinto-full.yaml")

    def test_round_trip(self):
        # Load some data
        cmd = "kinto-wizard {} --server={} --auth={}"
        load_cmd = cmd.format("load {}".format(self.file), self.server, self.auth)
        sys.argv = load_cmd.split(" ")
        main()

        cmd = "kinto-wizard {} --server={} --auth={} --data --collections --groups --records --permissions"
        load_cmd = cmd.format("dump", self.server, self.auth)
        sys.argv = load_cmd.split(" ")
        output = io.StringIO()
        with redirect_stdout(output):
            main()
        output.flush()

        # Check that identical to original file.
        generated = output.getvalue()
        with open(self.file) as f:
            assert_identical(f.read(), generated)


class BucketCollectionSelectionableDump(FunctionalTest):
    file = os.getenv("FILE", "tests/dumps/dump-full.yaml")

    def test_validate(self):
        self.validate()

    def test_round_trip_with_bucket_selection_on_load(self):
        self.load(bucket="natim")
        generated = self.dump()
        with open("tests/dumps/dump-natim.yaml") as f:
            assert_identical(f.read(), generated)

    def test_round_trip_with_bucket_selection(self):
        self.load()
        generated = self.dump(bucket="natim")
        with open("tests/dumps/dump-natim.yaml") as f:
            assert_identical(f.read(), generated)

    def test_round_trip_with_bucket_collection_selection_on_load(self):
        self.load(bucket="natim", collection="toto")
        generated = self.dump()
        with open("tests/dumps/dump-natim-toto-groups.yaml") as f:
            assert_identical(f.read(), generated)

    def test_round_trip_with_bucket_collection_selection(self):
        self.load()
        generated = self.dump(bucket="natim", collection="toto")
        with open("tests/dumps/dump-natim-toto.yaml") as f:
            assert_identical(f.read(), generated)

    def test_round_trip_with_collection_selection_on_load(self):
        self.load(collection="toto")
        generated = self.dump()
        with open("tests/dumps/dump-toto-groups.yaml") as f:
            assert_identical(f.read(), generated)

    def test_round_trip_with_collection_selection(self):
        self.load()
        generated = self.dump(collection="toto")
        with open("tests/dumps/dump-toto.yaml") as f:
            assert_identical(f.read(), generated)

    def test_wizard_can_handle_dates(self):
        self.load(bucket="date")
        generated = self.dump()
        with open("tests/dumps/dump-date.yaml") as f:
            assert_identical(f.read(), generated)


class YAMLReferenceSupportTest(FunctionalTest):
    file = os.getenv("FILE", "tests/dumps/with-references.yaml")

    def test_validate(self):
        self.validate()

    def test_file_can_have_yaml_references(self):
        self.load()

        client = Client(server_url=self.server, auth=tuple(self.auth.split(":")))

        collection = client.get_collection(bucket="main", id="certificates")
        assert "url" in collection["data"]["schema"]["properties"]
        collection = client.get_collection(bucket="main", id="addons")
        assert "url" in collection["data"]["schema"]["properties"]

        # the anchor did not get interpreted as a bucket:
        with self.assertRaises(exceptions.KintoException):
            client.get_collection(bucket="attachment-schema")


class WrongSchemaValidationTest(FunctionalTest):
    file = "tests/dumps/wrong-schema.yaml"

    def test_validate(self):
        self.validate(code=1)


class MiscUpdates(FunctionalTest):
    def get_client(self):
        return Client(server_url=self.server, auth=tuple(self.auth.split(":")))

    def test_validate(self):
        # This dump has a schema that requires `title` field, and a record doesn't have it.
        self.validate(filename="tests/dumps/with-schema-1.yaml", code=1)
        # This dump has a schema that does not require `title` field, so the dump is valid.
        self.validate(filename="tests/dumps/with-schema-2.yaml")

    def test_raises_with_4xx_error_in_batch(self):
        with pytest.raises(exceptions.KintoBatchException):
            self.load(filename="tests/dumps/with-schema-1.yaml")
        records = self.get_client().get_records(bucket="natim", collection="toto")
        assert len(records) == 0

    def test_ignore_batch_4xx_errors_if_specified(self):
        # Raises a KintoBatchException in case of error
        self.load(filename="tests/dumps/with-schema-1.yaml", extra="--ignore-batch-4xx")

    def test_record_updates(self):
        self.load(filename="tests/dumps/with-schema-1.yaml", extra="--ignore-batch-4xx")
        client = self.get_client()
        client.create_record(
            data={"title": "titi", "last_modified": 1496132479110},
            id="e2686bac-c45e-4144-9738-edfeb3d9da6d",
            collection="toto",
            bucket="natim",
        )
        self.load(filename="tests/dumps/with-schema-2.yaml")
        r = client.get_record(
            id="e2686bac-c45e-4144-9738-edfeb3d9da6d", collection="toto", bucket="natim"
        )
        assert r["data"]["title"] == "toto"

    def test_group_updates(self):
        self.load(filename="tests/dumps/with-groups.yaml")
        client = self.get_client()
        client.update_group(
            data={"members": ["alexis", "mathieu", "remy"]}, id="toto", bucket="natim"
        )
        self.load(filename="tests/dumps/with-groups.yaml")
        r = client.get_group(id="toto", bucket="natim")
        assert r["data"]["members"] == ["mathieu", "alexis"]

    def test_timestamps_are_not_changed_if_dump_hasnt_changed(self):
        self.load(filename="tests/dumps/with-groups.yaml")
        client = self.get_client()
        before = client.get_group(id="toto", bucket="natim")["data"]["last_modified"]

        self.load(filename="tests/dumps/with-groups.yaml")

        after = client.get_group(id="toto", bucket="natim")["data"]["last_modified"]
        assert before == after


class AttachmentsTest(FunctionalTest):
    def setUp(self):
        os.makedirs("/tmp/__attachments__/main/archives/", exist_ok=True)
        return super().setUp()

    def tearDown(self):
        shutil.rmtree("/tmp/__attachments__", ignore_errors=True)
        return super().tearDown()

    @property
    def client(self):
        return Client(server_url=self.server, auth=tuple(self.auth.split(":")))

    def _create_attachment_manually(self):
        self.client.create_bucket(id="main")
        self.client.create_collection(bucket="main", id="archives")
        return self.client.add_attachment(
            id="abc",
            bucket="main",
            collection="archives",
            filepath="tests/dumps/image.jpg",
            data={"title": "Test Attachment"},
        )

    def test_dump_with_attachments(self):
        self._create_attachment_manually()

        dumped = self.dump(extra="--attachments=/tmp/__attachments__")
        yaml = YAML(typ="safe")
        dumped_parsed = yaml.load(dumped)

        with open("tests/dumps/with-attachments.yaml") as f:
            expected = f.read()
        expected_parsed = yaml.load(expected)

        # The location is randomly assigned by server.
        real_location = dumped_parsed["buckets"]["main"]["collections"]["archives"]["records"][
            "abc"
        ]["data"]["attachment"]["location"]

        expected_parsed["buckets"]["main"]["collections"]["archives"]["records"]["abc"]["data"][
            "attachment"
        ]["location"] = real_location
        dumped_without_last_modified = deepcopy(dumped_parsed)
        del dumped_without_last_modified["buckets"]["main"]["data"]["last_modified"]
        del dumped_without_last_modified["buckets"]["main"]["collections"]["archives"]["data"][
            "last_modified"
        ]
        del dumped_without_last_modified["buckets"]["main"]["collections"]["archives"]["records"][
            "abc"
        ]["data"]["last_modified"]

        assert dumped_without_last_modified == expected_parsed
        assert os.path.exists("/tmp/__attachments__")
        assert os.path.exists(os.path.join("/tmp/__attachments__", real_location))

    def test_load_with_attachments_from_unexisting_folder(self):
        self._create_attachment_manually()

        self.load(filename="tests/dumps/with-attachments.yaml", extra="--attachments=/tmp/missing")

    def test_load_with_attachments_from_unexisting_folder_with_existing_record(self):
        self.load(filename="tests/dumps/with-attachments.yaml", extra="--attachments=/tmp/missing")

    def test_load_with_attachments_with_missing_record(self):
        shutil.copyfile(
            "tests/dumps/image.jpg",
            "/tmp/__attachments__/main/archives/aedddd6b-f6ef-423b-8e4c-ac23a74736c3.jpg",
        )

        self.load(
            filename="tests/dumps/with-attachments.yaml",
            extra="--attachments=/tmp/__attachments__",
        )

        record_after = self.client.get_record(bucket="main", collection="archives", id="abc")
        assert (
            record_after["data"]["attachment"]["filename"]
            == "aedddd6b-f6ef-423b-8e4c-ac23a74736c3.jpg"
        )
        assert record_after["data"]["attachment"]["size"] > 0

    def test_load_with_attachments_uses_filename_from_meta_file(self):
        location = "/tmp/__attachments__/main/archives/aedddd6b-f6ef-423b-8e4c-ac23a74736c3.jpg"
        shutil.copyfile("tests/dumps/image.jpg", location)
        with open(location + ".meta.json", "w") as f:
            json.dump({"attachment": {"filename": "image.jpg"}}, f)

        self.load(
            filename="tests/dumps/with-attachments.yaml",
            extra="--attachments=/tmp/__attachments__",
        )

        record_after = self.client.get_record(bucket="main", collection="archives", id="abc")
        assert record_after["data"]["attachment"]["filename"] == "image.jpg"
        assert record_after["data"]["attachment"]["size"] > 0

    def test_load_with_attachments_with_existing_record(self):
        attachment_before = self._create_attachment_manually()
        shutil.copyfile(
            "tests/dumps/image.jpg",
            "/tmp/__attachments__/main/archives/aedddd6b-f6ef-423b-8e4c-ac23a74736c3.jpg",
        )

        self.load(
            filename="tests/dumps/with-attachments.yaml",
            extra="--attachments=/tmp/__attachments__",
        )

        record_after = self.client.get_record(bucket="main", collection="archives", id="abc")
        assert attachment_before["hash"] == record_after["data"]["attachment"]["hash"]

    def test_load_with_attachments_with_existing_record_but_different(self):
        attachment_before = self._create_attachment_manually()
        with open(
            "/tmp/__attachments__/main/archives/aedddd6b-f6ef-423b-8e4c-ac23a74736c3.jpg", "wb"
        ) as f:
            f.write(b"Different content")

        self.load(
            filename="tests/dumps/with-attachments.yaml",
            extra="--attachments=/tmp/__attachments__",
        )

        record_after = self.client.get_record(bucket="main", collection="archives", id="abc")
        assert attachment_before["hash"] != record_after["data"]["attachment"]["hash"]


class PartialLoadTest(FunctionalTest):
    def test_load_only_groups(self):
        self.load(filename="tests/dumps/dump-full.yaml", extra="--groups")

    def test_load_only_permissions(self):
        self.load(filename="tests/dumps/dump-full.yaml", extra="--permissions")
