#
# Copyright (c) 2020, Neptune Labs Sp. z o.o.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

# pylint: disable=protected-access
import os
import unittest
import uuid
from datetime import datetime

from mock import Mock, patch

from neptune.new import ANONYMOUS, Run, get_last_run, get_project, init
from neptune.new.attributes.atoms import String
from neptune.new.envs import API_TOKEN_ENV_NAME, PROJECT_ENV_NAME
from neptune.new.exceptions import (
    MetadataInconsistency,
    NeptuneOfflineModeFetchException,
    NeptuneUninitializedException,
)
from neptune.new.internal.backends.api_model import (
    ApiRun,
    Attribute,
    AttributeType,
    AttributeWithProperties,
    LeaderboardEntry,
    IntAttribute,
)
from neptune.new.internal.backends.neptune_backend_mock import NeptuneBackendMock
from neptune.utils import IS_WINDOWS


@patch("neptune.new.internal.backends.factory.HostedNeptuneBackend", NeptuneBackendMock)
class TestClient(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        os.environ[PROJECT_ENV_NAME] = "organization/project"
        os.environ[API_TOKEN_ENV_NAME] = ANONYMOUS

    def test_incorrect_mode(self):
        with self.assertRaises(ValueError):
            init(mode="srtgj")

    def test_debug_mode(self):
        exp = init(mode="debug")
        exp["some/variable"] = 13
        self.assertEqual(13, exp["some/variable"].fetch())
        self.assertNotIn(str(exp._id), os.listdir(".neptune"))

    def test_offline_mode(self):
        exp = init(mode="offline")
        exp["some/variable"] = 13
        with self.assertRaises(NeptuneOfflineModeFetchException):
            exp["some/variable"].fetch()
        self.assertIn(str(exp._id), os.listdir(".neptune/offline"))
        self.assertIn("data-1.log", os.listdir(".neptune/offline/{}".format(exp._id)))

    def test_sync_mode(self):
        exp = init(mode="sync")
        exp["some/variable"] = 13
        self.assertEqual(13, exp["some/variable"].fetch())
        self.assertNotIn(str(exp._id), os.listdir(".neptune"))

    def test_async_mode(self):
        with init(mode="async", flush_period=0.5) as exp:
            exp["some/variable"] = 13
            with self.assertRaises(MetadataInconsistency):
                exp["some/variable"].fetch()
            exp.wait()
            self.assertEqual(13, exp["some/variable"].fetch())
            self.assertIn(str(exp._id), os.listdir(".neptune/async"))
            execution_dir = os.listdir(".neptune/async/{}".format(exp._id))[0]
            self.assertIn(
                "data-1.log",
                os.listdir(".neptune/async/{}/{}".format(exp._id, execution_dir)),
            )

    @patch(
        "neptune.new.internal.backends.neptune_backend_mock.NeptuneBackendMock.get_run",
        new=lambda _, _id: ApiRun(
            uuid.UUID("12345678-1234-5678-1234-567812345678"),
            "SAN-94",
            "workspace",
            "sandbox",
            False,
        ),
    )
    @patch(
        "neptune.new.internal.backends.neptune_backend_mock.NeptuneBackendMock.get_attributes",
        new=lambda _, _uuid: [Attribute("some/variable", AttributeType.INT)],
    )
    @patch(
        "neptune.new.internal.backends.neptune_backend_mock.NeptuneBackendMock.get_int_attribute",
        new=lambda _, _uuid, _path: IntAttribute(42),
    )
    def test_read_only_mode(self):
        exp = init(mode="read-only", run="SAN-94")

        with self.assertLogs() as caplog:
            exp["some/variable"] = 13
            exp["some/other_variable"] = 11
            self.assertEqual(
                caplog.output,
                [
                    "WARNING:neptune.new.internal.operation_processors.read_only_operation_processor:"
                    "Client in read-only mode, nothing will be saved to server."
                ],
            )

        self.assertEqual(42, exp["some/variable"].fetch())
        self.assertNotIn(str(exp._id), os.listdir(".neptune"))

    @patch(
        "neptune.new.internal.backends.neptune_backend_mock.NeptuneBackendMock.get_run",
        new=lambda _, _id: ApiRun(
            uuid.UUID("12345678-1234-5678-1234-567812345678"),
            "SAN-94",
            "workspace",
            "sandbox",
            False,
        ),
    )
    @patch(
        "neptune.new.internal.backends.neptune_backend_mock.NeptuneBackendMock.get_attributes",
        new=lambda _, _uuid: [Attribute("test", AttributeType.STRING)],
    )
    def test_resume(self):
        with init(flush_period=0.5, run="SAN-94") as exp:
            self.assertEqual(exp._id, uuid.UUID("12345678-1234-5678-1234-567812345678"))
            self.assertIsInstance(exp.get_structure()["test"], String)

    @patch("neptune.new.internal.utils.source_code.sys.argv", ["main.py"])
    @patch(
        "neptune.new.internal.init_impl.os.path.isfile", new=lambda file: "." in file
    )
    @patch(
        "neptune.new.internal.utils.glob",
        new=lambda path, recursive=False: [path.replace("*", "file.txt")],
    )
    @patch(
        "neptune.new.internal.utils.os.path.abspath",
        new=lambda path: os.path.normpath("/home/user/main_dir/" + path),
    )
    @patch("neptune.new.internal.utils.os.getcwd", new=lambda: "/home/user/main_dir")
    @unittest.skipIf(IS_WINDOWS, "Linux/Mac test")
    def test_entrypoint(self):
        exp = init(mode="debug")
        self.assertEqual(exp["source_code/entrypoint"].fetch(), "main.py")

        exp = init(mode="debug", source_files=[])
        self.assertEqual(exp["source_code/entrypoint"].fetch(), "main.py")

        exp = init(mode="debug", source_files=["../*"])
        self.assertEqual(exp["source_code/entrypoint"].fetch(), "main_dir/main.py")

        exp = init(mode="debug", source_files=["internal/*"])
        self.assertEqual(exp["source_code/entrypoint"].fetch(), "main.py")

        exp = init(mode="debug", source_files=["../other_dir/*"])
        self.assertEqual(exp["source_code/entrypoint"].fetch(), "../main_dir/main.py")

    @patch("neptune.new.internal.utils.source_code.sys.argv", ["main.py"])
    @patch("neptune.new.internal.utils.source_code.is_ipython", new=lambda: True)
    def test_entrypoint_in_interactive_python(self):
        exp = init(mode="debug")
        with self.assertRaises(AttributeError):
            exp["source_code/entrypoint"].get()

        exp = init(mode="debug", source_files=[])
        with self.assertRaises(AttributeError):
            exp["source_code/entrypoint"].get()

        exp = init(mode="debug", source_files=["../*"])
        with self.assertRaises(AttributeError):
            exp["source_code/entrypoint"].get()

        exp = init(mode="debug", source_files=["internal/*"])
        with self.assertRaises(AttributeError):
            exp["source_code/entrypoint"].get()

    @patch("neptune.new.internal.utils.source_code.sys.argv", ["main.py"])
    @patch("neptune.new.internal.utils.source_code.get_common_root", new=lambda _: None)
    @patch(
        "neptune.new.internal.init_impl.os.path.isfile", new=lambda file: "." in file
    )
    @patch(
        "neptune.new.internal.utils.glob",
        new=lambda path, recursive=False: [path.replace("*", "file.txt")],
    )
    @patch(
        "neptune.new.internal.utils.os.path.abspath",
        new=lambda path: os.path.normpath("/home/user/main_dir/" + path),
    )
    def test_entrypoint_without_common_root(self):
        exp = init(mode="debug", source_files=["../*"])
        self.assertEqual(
            exp["source_code/entrypoint"].fetch(), "/home/user/main_dir/main.py"
        )

        exp = init(mode="debug", source_files=["internal/*"])
        self.assertEqual(
            exp["source_code/entrypoint"].fetch(), "/home/user/main_dir/main.py"
        )

    @patch("neptune.new.internal.get_project_impl.HostedNeptuneBackend")
    def test_get_table_as_pandas(self, backend_init_mock):
        # given
        backend_mock = Mock()
        backend_init_mock.return_value = backend_mock

        # and
        attributes = []
        now = datetime.now()
        attributes.append(
            AttributeWithProperties(
                "run/state", AttributeType.RUN_STATE, Mock(value="idle")
            )
        )
        attributes.append(
            AttributeWithProperties("float", AttributeType.FLOAT, Mock(value=12.5))
        )
        attributes.append(
            AttributeWithProperties(
                "string", AttributeType.STRING, Mock(value="some text")
            )
        )
        attributes.append(
            AttributeWithProperties("datetime", AttributeType.DATETIME, Mock(value=now))
        )
        attributes.append(
            AttributeWithProperties(
                "float/series", AttributeType.FLOAT_SERIES, Mock(last=8.7)
            )
        )
        attributes.append(
            AttributeWithProperties(
                "string/series", AttributeType.STRING_SERIES, Mock(last="last text")
            )
        )
        attributes.append(
            AttributeWithProperties(
                "string/set", AttributeType.STRING_SET, Mock(values=["a", "b"])
            )
        )
        attributes.append(
            AttributeWithProperties(
                "git/ref",
                AttributeType.GIT_REF,
                Mock(commit=Mock(commitId="abcdef0123456789")),
            )
        )
        attributes.append(AttributeWithProperties("file", AttributeType.FILE, None))
        attributes.append(
            AttributeWithProperties("file/set", AttributeType.FILE_SET, None)
        )
        attributes.append(
            AttributeWithProperties("image/series", AttributeType.IMAGE_SERIES, None)
        )

        # and
        empty_entry = LeaderboardEntry(uuid.uuid4(), [])
        filled_entry = LeaderboardEntry(uuid.uuid4(), attributes)
        backend_mock.get_leaderboard = Mock(return_value=[empty_entry, filled_entry])

        # when
        df = get_project().fetch_runs_table().to_pandas()

        # then
        self.assertEqual("idle", df["run/state"][1])
        self.assertEqual(12.5, df["float"][1])
        self.assertEqual("some text", df["string"][1])
        self.assertEqual(now, df["datetime"][1])
        self.assertEqual(8.7, df["float/series"][1])
        self.assertEqual("last text", df["string/series"][1])
        self.assertEqual("a,b", df["string/set"][1])
        self.assertEqual("abcdef0123456789", df["git/ref"][1])

        with self.assertRaises(KeyError):
            self.assertTrue(df["file"])
        with self.assertRaises(KeyError):
            self.assertTrue(df["file/set"])
        with self.assertRaises(KeyError):
            self.assertTrue(df["image/series"])

    @patch("neptune.new.internal.get_project_impl.HostedNeptuneBackend")
    def test_get_table_as_runs(self, backend_init_mock):
        # given
        backend_mock = Mock()
        backend_init_mock.return_value = backend_mock

        # and
        exp_id = uuid.uuid4()
        attributes = []
        now = datetime.now()
        attributes.append(
            AttributeWithProperties(
                "run/state", AttributeType.RUN_STATE, Mock(value="idle")
            )
        )
        attributes.append(
            AttributeWithProperties("float", AttributeType.FLOAT, Mock(value=12.5))
        )
        attributes.append(
            AttributeWithProperties(
                "string", AttributeType.STRING, Mock(value="some text")
            )
        )
        attributes.append(
            AttributeWithProperties("datetime", AttributeType.DATETIME, Mock(value=now))
        )
        attributes.append(
            AttributeWithProperties(
                "float/series", AttributeType.FLOAT_SERIES, Mock(last=8.7)
            )
        )
        attributes.append(
            AttributeWithProperties(
                "string/series", AttributeType.STRING_SERIES, Mock(last="last text")
            )
        )
        attributes.append(
            AttributeWithProperties(
                "string/set", AttributeType.STRING_SET, Mock(values=["a", "b"])
            )
        )
        attributes.append(
            AttributeWithProperties(
                "git/ref",
                AttributeType.GIT_REF,
                Mock(commit=Mock(commitId="abcdef0123456789")),
            )
        )
        attributes.append(AttributeWithProperties("file", AttributeType.FILE, None))
        attributes.append(
            AttributeWithProperties("file/set", AttributeType.FILE_SET, None)
        )
        attributes.append(
            AttributeWithProperties("image/series", AttributeType.IMAGE_SERIES, None)
        )

        # and
        backend_mock.get_leaderboard = Mock(
            return_value=[LeaderboardEntry(exp_id, attributes)]
        )

        # when
        exp = get_project().fetch_runs_table().to_runs()[0]

        # then
        self.assertEqual("idle", exp["run/state"].get())
        self.assertEqual("idle", exp["run"]["state"].get())
        self.assertEqual(12.5, exp["float"].get())
        self.assertEqual("some text", exp["string"].get())
        self.assertEqual(now, exp["datetime"].get())
        self.assertEqual(8.7, exp["float/series"].get())
        self.assertEqual("last text", exp["string/series"].get())
        self.assertEqual({"a", "b"}, exp["string/set"].get())
        self.assertEqual("abcdef0123456789", exp["git/ref"].get())

        with self.assertRaises(MetadataInconsistency):
            exp["file"].get()
        with self.assertRaises(MetadataInconsistency):
            exp["file/set"].get()
        with self.assertRaises(MetadataInconsistency):
            exp["image/series"].get()

        exp["file"].download("some_directory")
        backend_mock.download_file.assert_called_with(
            exp_id, ["file"], "some_directory"
        )

        exp["file/set"].download("some_directory")
        backend_mock.download_file_set.assert_called_with(
            exp_id, ["file", "set"], "some_directory"
        )

    def test_last_exp_is_raising_exception_when_non_initialized(self):
        # given uninitialized run
        Run.last_run = None

        # expect: raises NeptuneUninitializedException
        with self.assertRaises(NeptuneUninitializedException):
            get_last_run()

    def test_last_exp_is_the_latest_initialized(self):
        # given two initialized runs
        with init() as exp1, init() as exp2:
            # expect: `neptune.latest_run` to be the latest initialized one
            self.assertIsNot(exp1, get_last_run())
            self.assertIs(exp2, get_last_run())
