# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.

"""Tests for the command-line interface."""

import os
import platform
import sys
import unittest
from pathlib import Path
from unittest.mock import patch

import pytest
from confuse import ConfigError

from beets import config, plugins, ui
from beets.test import _common
from beets.test.helper import BeetsTestCase, IOMixin, PluginTestCase
from beets.ui import commands
from beets.util import syspath


class PrintTest(IOMixin, unittest.TestCase):
    def test_print_without_locale(self):
        lang = os.environ.get("LANG")
        if lang:
            del os.environ["LANG"]

        try:
            ui.print_("something")
        except TypeError:
            self.fail("TypeError during print")
        finally:
            if lang:
                os.environ["LANG"] = lang

    def test_print_with_invalid_locale(self):
        old_lang = os.environ.get("LANG")
        os.environ["LANG"] = ""
        old_ctype = os.environ.get("LC_CTYPE")
        os.environ["LC_CTYPE"] = "UTF-8"

        try:
            ui.print_("something")
        except ValueError:
            self.fail("ValueError during print")
        finally:
            if old_lang:
                os.environ["LANG"] = old_lang
            else:
                del os.environ["LANG"]
            if old_ctype:
                os.environ["LC_CTYPE"] = old_ctype
            else:
                del os.environ["LC_CTYPE"]


class ShowModelChangesTest(IOMixin, BeetsTestCase):
    def test_uses_database_state_when_old_not_provided(self):
        item = self.add_item_fixture(title="old title")
        old_label = format(item.get_fresh_from_db())

        item.title = "new title"

        assert ui.show_model_changes(item) is True
        assert self.io.getoutput().splitlines() == [
            old_label,
            "  title: old title -> new title",
        ]


@_common.slow_test()
class TestPluginTestCase(PluginTestCase):
    plugin = "test"

    def setUp(self):
        self.config["pluginpath"] = [_common.PLUGINPATH]
        super().setUp()


class ConfigTest(IOMixin, TestPluginTestCase):
    def setUp(self):
        super().setUp()

        # Don't use the BEETSDIR from `helper`. Instead, we point the home
        # directory there. Some tests will set `BEETSDIR` themselves.
        del os.environ["BEETSDIR"]

        # Also set APPDATA, the Windows equivalent of setting $HOME.
        appdata_dir = self.temp_dir_path / "AppData" / "Roaming"

        self._orig_cwd = os.getcwd()
        self.test_cmd = self._make_test_cmd()
        commands.default_commands.append(self.test_cmd)

        # Default user configuration
        if platform.system() == "Windows":
            self.user_config_dir = appdata_dir / "beets"
        else:
            self.user_config_dir = self.temp_dir_path / ".config" / "beets"
        self.user_config_dir.mkdir(parents=True, exist_ok=True)
        self.user_config_path = self.user_config_dir / "config.yaml"

        # Custom BEETSDIR
        self.beetsdir = self.temp_dir_path / "beetsdir"
        self.beetsdir.mkdir(parents=True, exist_ok=True)

        self.env_config_path = str(self.beetsdir / "config.yaml")
        self.cli_config_path = str(self.temp_dir_path / "config.yaml")
        self.env_patcher = patch(
            "os.environ",
            {"HOME": str(self.temp_dir_path), "APPDATA": str(appdata_dir)},
        )
        self.env_patcher.start()

        self._reset_config()

    def tearDown(self):
        self.env_patcher.stop()
        commands.default_commands.pop()
        os.chdir(syspath(self._orig_cwd))
        super().tearDown()

    def _make_test_cmd(self):
        test_cmd = ui.Subcommand("test", help="test")

        def run(lib, options, args):
            test_cmd.lib = lib
            test_cmd.options = options
            test_cmd.args = args

        test_cmd.func = run
        return test_cmd

    def _reset_config(self):
        # Config should read files again on demand
        config.clear()
        config._materialized = False

    def write_config_file(self):
        return open(self.user_config_path, "w")

    def test_paths_section_respected(self):
        with self.write_config_file() as config:
            config.write("paths: {x: y}")

        self.run_command("test", lib=None)
        key, template = self.test_cmd.lib.path_formats[0]
        assert key == "x"
        assert template.original == "y"

    def test_default_paths_preserved(self):
        default_formats = ui.get_path_formats()

        self._reset_config()
        with self.write_config_file() as config:
            config.write("paths: {x: y}")
        self.run_command("test", lib=None)
        key, template = self.test_cmd.lib.path_formats[0]
        assert key == "x"
        assert template.original == "y"
        assert self.test_cmd.lib.path_formats[1:] == default_formats

    def test_nonexistant_db(self):
        with self.write_config_file() as config:
            config.write("library: /xxx/yyy/not/a/real/path")

        self.io.addinput("n")
        with pytest.raises(ui.UserError):
            self.run_command("test", lib=None)

    def test_user_config_file(self):
        with self.write_config_file() as file:
            file.write("anoption: value")

        self.run_command("test", lib=None)
        assert config["anoption"].get() == "value"

    def test_replacements_parsed(self):
        with self.write_config_file() as config:
            config.write("replace: {'[xy]': z}")

        self.run_command("test", lib=None)
        replacements = self.test_cmd.lib.replacements
        repls = [(p.pattern, s) for p, s in replacements]  # Compare patterns.
        assert repls == [("[xy]", "z")]

    def test_multiple_replacements_parsed(self):
        with self.write_config_file() as config:
            config.write("replace: {'[xy]': z, foo: bar}")
        self.run_command("test", lib=None)
        replacements = self.test_cmd.lib.replacements
        repls = [(p.pattern, s) for p, s in replacements]
        assert repls == [("[xy]", "z"), ("foo", "bar")]

    def test_cli_config_option(self):
        with open(self.cli_config_path, "w") as file:
            file.write("anoption: value")
        self.run_command("--config", self.cli_config_path, "test", lib=None)
        assert config["anoption"].get() == "value"

    def test_cli_config_file_overwrites_user_defaults(self):
        with open(self.user_config_path, "w") as file:
            file.write("anoption: value")

        with open(self.cli_config_path, "w") as file:
            file.write("anoption: cli overwrite")
        self.run_command("--config", self.cli_config_path, "test", lib=None)
        assert config["anoption"].get() == "cli overwrite"

    def test_cli_config_file_overwrites_beetsdir_defaults(self):
        os.environ["BEETSDIR"] = str(self.beetsdir)
        with open(self.env_config_path, "w") as file:
            file.write("anoption: value")

        with open(self.cli_config_path, "w") as file:
            file.write("anoption: cli overwrite")
        self.run_command("--config", self.cli_config_path, "test", lib=None)
        assert config["anoption"].get() == "cli overwrite"

    #    @unittest.skip('Difficult to implement with optparse')
    #    def test_multiple_cli_config_files(self):
    #        cli_config_path_1 = os.path.join(self.temp_dir, b'config.yaml')
    #        cli_config_path_2 = os.path.join(self.temp_dir, b'config_2.yaml')
    #
    #        with open(cli_config_path_1, 'w') as file:
    #            file.write('first: value')
    #
    #        with open(cli_config_path_2, 'w') as file:
    #            file.write('second: value')
    #
    #        self.run_command('--config', cli_config_path_1,
    #                      '--config', cli_config_path_2, 'test', lib=None)
    #        assert config['first'].get() == 'value'
    #        assert config['second'].get() == 'value'
    #
    #    @unittest.skip('Difficult to implement with optparse')
    #    def test_multiple_cli_config_overwrite(self):
    #        cli_overwrite_config_path = os.path.join(self.temp_dir,
    #                                                 b'overwrite_config.yaml')
    #
    #        with open(self.cli_config_path, 'w') as file:
    #            file.write('anoption: value')
    #
    #        with open(cli_overwrite_config_path, 'w') as file:
    #            file.write('anoption: overwrite')
    #
    #        self.run_command('--config', self.cli_config_path,
    #                      '--config', cli_overwrite_config_path, 'test')
    #        assert config['anoption'].get() == 'cli overwrite'

    # FIXME: fails on windows
    @unittest.skipIf(sys.platform == "win32", "win32")
    def test_cli_config_paths_resolve_relative_to_user_dir(self):
        with open(self.cli_config_path, "w") as file:
            file.write("library: beets.db\n")
            file.write("statefile: state")

        self.run_command("--config", self.cli_config_path, "test", lib=None)
        assert config["library"].as_path() == self.user_config_dir / "beets.db"
        assert config["statefile"].as_path() == self.user_config_dir / "state"

    def test_cli_config_paths_resolve_relative_to_beetsdir(self):
        os.environ["BEETSDIR"] = str(self.beetsdir)

        with open(self.cli_config_path, "w") as file:
            file.write("library: beets.db\n")
            file.write("statefile: state")

        self.run_command("--config", self.cli_config_path, "test", lib=None)
        assert config["library"].as_path() == self.beetsdir / "beets.db"
        assert config["statefile"].as_path() == self.beetsdir / "state"

    def test_command_line_option_relative_to_working_dir(self):
        config.read()
        os.chdir(syspath(self.temp_dir))
        self.run_command("--library", "foo.db", "test", lib=None)
        assert config["library"].as_path() == Path.cwd() / "foo.db"

    def test_cli_config_file_loads_plugin_commands(self):
        with open(self.cli_config_path, "w") as file:
            file.write(f"pluginpath: {_common.PLUGINPATH}\n")
            file.write("plugins: test")

        self.run_command("--config", self.cli_config_path, "plugin", lib=None)
        plugs = plugins.find_plugins()
        assert len(plugs) == 1
        assert plugs[0].is_test_plugin
        self.unload_plugins()

    def test_beetsdir_config(self):
        os.environ["BEETSDIR"] = str(self.beetsdir)

        with open(self.env_config_path, "w") as file:
            file.write("anoption: overwrite")

        config.read()
        assert config["anoption"].get() == "overwrite"

    def test_beetsdir_points_to_file_error(self):
        beetsdir = str(self.temp_dir_path / "beetsfile")
        open(beetsdir, "a").close()
        os.environ["BEETSDIR"] = beetsdir
        with pytest.raises(ConfigError):
            self.run_command("test")

    def test_beetsdir_config_does_not_load_default_user_config(self):
        os.environ["BEETSDIR"] = str(self.beetsdir)

        with open(self.user_config_path, "w") as file:
            file.write("anoption: value")

        config.read()
        assert not config["anoption"].exists()

    def test_default_config_paths_resolve_relative_to_beetsdir(self):
        os.environ["BEETSDIR"] = str(self.beetsdir)

        config.read()
        assert config["library"].as_path() == self.beetsdir / "library.db"
        assert config["statefile"].as_path() == self.beetsdir / "state.pickle"

    def test_beetsdir_config_paths_resolve_relative_to_beetsdir(self):
        os.environ["BEETSDIR"] = str(self.beetsdir)

        with open(self.env_config_path, "w") as file:
            file.write("library: beets.db\n")
            file.write("statefile: state")

        config.read()
        assert config["library"].as_path() == self.beetsdir / "beets.db"
        assert config["statefile"].as_path() == self.beetsdir / "state"


class PathFormatTest(unittest.TestCase):
    def test_custom_paths_prepend(self):
        default_formats = ui.get_path_formats()

        config["paths"] = {"foo": "bar"}
        pf = ui.get_path_formats()
        key, tmpl = pf[0]
        assert key == "foo"
        assert tmpl.original == "bar"
        assert pf[1:] == default_formats


@_common.slow_test()
class PluginTest(TestPluginTestCase):
    def test_plugin_command_from_pluginpath(self):
        self.run_command("test", lib=None)


class CommonOptionsParserCliTest(IOMixin, BeetsTestCase):
    """Test CommonOptionsParser and formatting LibModel formatting on 'list'
    command.
    """

    def setUp(self):
        super().setUp()
        self.item = _common.item()
        self.item.path = b"xxx/yyy"
        self.lib.add(self.item)
        self.lib.add_album([self.item])

    def test_base(self):
        output = self.run_with_output("ls")
        assert output == "the artist - the album - the title\n"

        output = self.run_with_output("ls", "-a")
        assert output == "the album artist - the album\n"

    def test_path_option(self):
        output = self.run_with_output("ls", "-p")
        assert output == f"{self.lib_path / 'xxx/yyy'}\n"

        output = self.run_with_output("ls", "-a", "-p")
        assert output == f"{self.lib_path / 'xxx'}\n"

    def test_format_option(self):
        output = self.run_with_output("ls", "-f", "$artist")
        assert output == "the artist\n"

        output = self.run_with_output("ls", "-a", "-f", "$albumartist")
        assert output == "the album artist\n"

    def test_format_option_unicode(self):
        output = self.run_with_output("ls", "-f", "caf\xe9")
        assert output == "caf\xe9\n"

    def test_root_format_option(self):
        output = self.run_with_output(
            "--format-item", "$artist", "--format-album", "foo", "ls"
        )
        assert output == "the artist\n"

        output = self.run_with_output(
            "--format-item", "foo", "--format-album", "$albumartist", "ls", "-a"
        )
        assert output == "the album artist\n"

    def test_help(self):
        output = self.run_with_output("help")
        assert "Usage:" in output

        output = self.run_with_output("help", "list")
        assert "Usage:" in output

        with pytest.raises(ui.UserError):
            self.run_command("help", "this.is.not.a.real.command")

    def test_stats(self):
        output = self.run_with_output("stats")
        assert "Approximate total size:" in output

        # # Need to have more realistic library setup for this to work
        # output = self.run_with_output('stats', '-e')
        # assert 'Total size:' in output

    def test_version(self):
        output = self.run_with_output("version")
        assert "Python version" in output
        assert "no plugins loaded" in output

        # # Need to have plugin loaded
        # output = self.run_with_output('version')
        # assert 'plugins: ' in output


class CommonOptionsParserTest(unittest.TestCase):
    def test_album_option(self):
        parser = ui.CommonOptionsParser()
        assert not parser._album_flags
        parser.add_album_option()
        assert bool(parser._album_flags)

        assert parser.parse_args([]) == ({"album": None}, [])
        assert parser.parse_args(["-a"]) == ({"album": True}, [])
        assert parser.parse_args(["--album"]) == ({"album": True}, [])

    def test_path_option(self):
        parser = ui.CommonOptionsParser()
        parser.add_path_option()
        assert not parser._album_flags

        config["format_item"].set("$foo")
        assert parser.parse_args([]) == ({"path": None}, [])
        assert config["format_item"].as_str() == "$foo"

        assert parser.parse_args(["-p"]) == (
            {"path": True, "format": "$path"},
            [],
        )
        assert parser.parse_args(["--path"]) == (
            {"path": True, "format": "$path"},
            [],
        )

        assert config["format_item"].as_str() == "$path"
        assert config["format_album"].as_str() == "$path"

    def test_format_option(self):
        parser = ui.CommonOptionsParser()
        parser.add_format_option()
        assert not parser._album_flags

        config["format_item"].set("$foo")
        assert parser.parse_args([]) == ({"format": None}, [])
        assert config["format_item"].as_str() == "$foo"

        assert parser.parse_args(["-f", "$bar"]) == ({"format": "$bar"}, [])
        assert parser.parse_args(["--format", "$baz"]) == (
            {"format": "$baz"},
            [],
        )

        assert config["format_item"].as_str() == "$baz"
        assert config["format_album"].as_str() == "$baz"

    def test_format_option_with_target(self):
        with pytest.raises(KeyError):
            ui.CommonOptionsParser().add_format_option(target="thingy")

        parser = ui.CommonOptionsParser()
        parser.add_format_option(target="item")

        config["format_item"].set("$item")
        config["format_album"].set("$album")

        assert parser.parse_args(["-f", "$bar"]) == ({"format": "$bar"}, [])

        assert config["format_item"].as_str() == "$bar"
        assert config["format_album"].as_str() == "$album"

    def test_format_option_with_album(self):
        parser = ui.CommonOptionsParser()
        parser.add_album_option()
        parser.add_format_option()

        config["format_item"].set("$item")
        config["format_album"].set("$album")

        parser.parse_args(["-f", "$bar"])
        assert config["format_item"].as_str() == "$bar"
        assert config["format_album"].as_str() == "$album"

        parser.parse_args(["-a", "-f", "$foo"])
        assert config["format_item"].as_str() == "$bar"
        assert config["format_album"].as_str() == "$foo"

        parser.parse_args(["-f", "$foo2", "-a"])
        assert config["format_album"].as_str() == "$foo2"

    def test_add_all_common_options(self):
        parser = ui.CommonOptionsParser()
        parser.add_all_common_options()
        assert parser.parse_args([]) == (
            {"album": None, "path": None, "format": None},
            [],
        )


class EncodingTest(unittest.TestCase):
    """Tests for the `terminal_encoding` config option and our
    `_in_encoding` and `_out_encoding` utility functions.
    """

    def out_encoding_overridden(self):
        config["terminal_encoding"] = "fake_encoding"
        assert ui._out_encoding() == "fake_encoding"

    def in_encoding_overridden(self):
        config["terminal_encoding"] = "fake_encoding"
        assert ui._in_encoding() == "fake_encoding"

    def out_encoding_default_utf8(self):
        with patch("sys.stdout") as stdout:
            stdout.encoding = None
            assert ui._out_encoding() == "utf-8"

    def in_encoding_default_utf8(self):
        with patch("sys.stdin") as stdin:
            stdin.encoding = None
            assert ui._in_encoding() == "utf-8"
