<filename>tests/test_cli.py
import csv
import itertools
import json
import tempfile
import re

import pytest

from browser_history.utils import get_browsers
from browser_history.cli import cli, AVAILABLE_BROWSERS
from .utils import (  # noqa: F401
    become_linux,
    become_mac,
    become_windows,
    change_homedir,
)

# pylint: disable=redefined-outer-name,unused-argument

CMD_ROOT = "browser-history"

# Options:
VALID_CMD_OPTS = [
    (f"--{long_o}", f"-{short_o}")
    for long_o, short_o in [
        ("help", "h"),
        ("type", "t"),
        ("browser", "b"),
        ("format", "f"),
        ("output", "o"),
    ]
]
INVALID_CMD_OPTS = [
    "--history",
    "-a",
    "-B",
    "-0",
]

# Arguments, where the default arg should be put first for default testing:
VALID_TYPE_ARGS = [
    "history",
    "bookmarks",
]
INVALID_TYPE_ARGS = ["cookies", "lol", "histories", "bookmark"]
VALID_BROWSER_ARGS = [
    "all",
    "Chrome",
    "Chromium",
    "Firefox",
    "Safari",
    "Edge",
    "Opera",
    "OperaGX",
    "Brave",
]
INVALID_BROWSER_ARGS = ["explorer", "ie", "netscape", "none", "brr"]
VALID_FORMAT_ARGS = [
    "infer",
    "csv",
    "json",
    "jsonl",
]
INVALID_FORMAT_ARGS = ["csvv", "txt", "html", "qwerty"]
# (VALID_OUTPUT_ARGS is any existent file, so validity depends on the fs)
GENERAL_INVALID_ARGS = [
    "foo",
    "0",
]

# Output markers:
HELP_SIGNATURE = (
    "usage: browser-history [-h] [-t TYPE] [-b BROWSER] [-f FORMAT] [-o OUTPUT]"
)
HISTORY_HEADER = "Timestamp,URL"
BOOKMARKS_HEADER = "Timestamp,URL,Title,Folder"

CSV_HISTORY_HEADER = HISTORY_HEADER
CSV_BOOKMARKS_HEADER = BOOKMARKS_HEADER


platform_fixture_map = {
    "linux": become_linux,
    "windows": become_windows,
    "mac": become_mac,
}
all_platform_fixtures = tuple("become_" + plat for plat in platform_fixture_map.keys())


@pytest.fixture
def platform(request):
    """Fixture to change platform based on pytest parameter"""
    request.getfixturevalue("change_homedir")
    return request.getfixturevalue(request.param)


def _get_browser_available_on_system():
    """Find a browser that is supported and installed on the test system."""
    for browser in get_browsers():
        try:  # filter out unsupported browsers
            history = browser().fetch_history()
        except AssertionError:  # anything caught here is unsupported
            continue
        # Has non-empty history so must be installed as well as supported
        if json.loads(history.to_json())["history"]:
            # Return string name which corresponds to a VALID_BROWSER_ARGS entry
            return browser.name
    return  # None indicating no browsers available on system


@pytest.mark.parametrize("platform", all_platform_fixtures, indirect=True)
def test_no_argument(capsys, platform):
    """Test the root command gives basic output."""
    cli([])
    captured = capsys.readouterr()
    assert CSV_HISTORY_HEADER in captured.out


@pytest.mark.parametrize("platform", all_platform_fixtures, indirect=True)
def test_type_argument(capsys, platform):
    """Test arguments for the type option."""
    for type_opt in VALID_CMD_OPTS[1]:
        for type_arg in VALID_TYPE_ARGS:
            cli([type_opt, type_arg])
            captured = capsys.readouterr()
            if type_arg == "history":
                # assuming csv is default
                assert captured.out.startswith(CSV_HISTORY_HEADER)
            if type_arg == "bookmarks":
                assert captured.out.startswith(CSV_BOOKMARKS_HEADER)


@pytest.mark.parametrize("browser_arg", VALID_BROWSER_ARGS)
@pytest.mark.parametrize("platform", all_platform_fixtures, indirect=True)
def test_browser_argument(browser_arg, capsys, platform):
    """Test arguments for the browser option."""
    for browser_opt in VALID_CMD_OPTS[2]:
        try:
            cli([browser_opt, browser_arg])
            captured = capsys.readouterr()
            assert CSV_HISTORY_HEADER in captured.out
        except AssertionError as e:
            if any(
                browser_unavailable_err in e.args[0]
                for browser_unavailable_err in (
                    "browser is not supported",
                    "browser is not installed",
                )
            ):
                # In case the tester does not have access to the browser
                # in question, which makes the command fail, but in a way
                # that is expected and gives a recognised error message.
                pytest.skip(
                    "Unable to test against {} browser because it is "
                    "not available locally".format(browser_opt)
                )
            else:  # command fails for another reason i.e. a test failure
                pytest.fail(
                    "{} browser is available but the command to fetch "
                    "the history from it has failed.".format(browser_opt)
                )


@pytest.mark.parametrize("platform", all_platform_fixtures, indirect=True)
def test_format_argument(capsys, platform):
    """Tests arguments for the format option."""
    # First check format of default:
    cli([])
    captured = capsys.readouterr()
    csv_output = captured.out
    # Sniffer determines format, less intensive than reading in csv.reader
    # and we don't mind the CSV dialect, so just check call doesn't error
    read_csv = csv.Sniffer()
    # This gives '_csv.Error: Could not determine delimiter' if not a csv file
    read_csv.sniff(csv_output, delimiters=",")
    assert read_csv.has_header(
        csv_output
    ), "CSV format missing heading with type followed by column names."

    for fmt_opt in VALID_CMD_OPTS[3]:
        for fmt_arg in VALID_FORMAT_ARGS:
            cli([fmt_opt, fmt_arg])
            output = capsys.readouterr().out
            if fmt_arg in ("csv", "infer"):  # infer gives csv if no file
                read_csv.sniff(output, delimiters=",")
                assert read_csv.has_header(output)
                assert CSV_HISTORY_HEADER in output
            elif fmt_arg == "json":
                json.loads(output)
            elif fmt_arg == "jsonl":
                # Newline-delimited json so test each line is valid json
                for line in output.splitlines():
                    json.loads(line)


@pytest.mark.parametrize("platform", all_platform_fixtures, indirect=True)
def test_output_argument(capsys, platform):
    """Test arguments for the output option."""
    for output_opt in VALID_CMD_OPTS[4]:
        with tempfile.TemporaryDirectory() as tmpdir:
            cli([output_opt, tmpdir + "out.csv"])
            output = capsys.readouterr().out
            # Check output was not sent to STDOUT since should go to file
            assert HISTORY_HEADER not in output
            with open(tmpdir + "out.csv", "rt") as f:  # now check the file
                assert HISTORY_HEADER in f.read()


@pytest.mark.parametrize("platform", all_platform_fixtures, indirect=True)
@pytest.mark.parametrize("browser", AVAILABLE_BROWSERS.split(", "))
def test_argument_combinations(capsys, platform, browser):
    """Test that combinations of optional arguments work properly."""
    # Choose some representative combinations. No need to test every one.
    indices = (0, 1)
    available_browser = _get_browser_available_on_system()
    # To test combos of short or long option variants
    for index_a, index_b in itertools.product(indices, indices):
        if available_browser:
            try:
                cli(
                    [
                        VALID_CMD_OPTS[1][index_a],  # type
                        VALID_TYPE_ARGS[1],  # ... is bookmarks,
                        VALID_CMD_OPTS[2][index_a],  # browser
                        browser,  # ... is any usable on system
                    ]
                )
            except AssertionError as e:
                if any(
                    browser_unavailable_err in e.args[0]
                    for browser_unavailable_err in (
                        "browser is not supported",
                        "browser is not installed",
                        "Bookmarks are not supported",
                    )
                ):
                    # In case the tester does not have access to the browser
                    # in question, which makes the command fail, but in a way
                    # that is expected and gives a recognised error message.
                    pytest.skip(
                        "Unable to test against {} browser because it is "
                        "not available locally".format(browser)
                    )
                else:  # command fails for another reason i.e. a test failure
                    pytest.fail(
                        "{} browser is available but the command to fetch "
                        "the history from it has failed.".format(browser)
                    )
            output = capsys.readouterr().out

            read_csv = csv.Sniffer()
            read_csv.sniff(output, delimiters=",")
            assert read_csv.has_header(output)
        else:  # unlikely but catch just in case can't use any browser
            for _ in range(3):  # to cover all three assert tests above
                pytest.skip("No browsers available to test with")
        with tempfile.TemporaryDirectory() as tmpdir:
            # This command should write to the given output file:
            cli(
                [
                    VALID_CMD_OPTS[3][index_b],  # format
                    VALID_FORMAT_ARGS[2],  # ... is json,
                    VALID_CMD_OPTS[4][index_b],  # output
                    tmpdir + "out.json",  # ... is a named file
                ]
            )
            with open(tmpdir + "out.json", "rb") as f:
                json.loads(f.read().decode("utf-8"))


def test_help_option(capsys):
    """Test the command-line help provided to the user on request."""
    for help_opt in VALID_CMD_OPTS[0]:
        with pytest.raises(SystemExit) as e:
            cli([help_opt])
            output = capsys.readouterr()
            assert HELP_SIGNATURE in output
            assert e.value.code == 0


def test_invalid_options(change_homedir):  # noqa: F811
    """Test that invalid options error correctly."""
    for bad_opt in INVALID_CMD_OPTS:
        with pytest.raises(SystemExit) as e:
            cli([bad_opt])
            assert e.value.code == 2


def test_invalid_format(change_homedir):  # noqa: F811
    """Test that invalid formats error correctly."""
    for bad_format in INVALID_FORMAT_ARGS:
        with pytest.raises(SystemExit) as e:
            cli(["-f", bad_format])
            assert e.value.code == 1


def test_invalid_type(change_homedir):  # noqa: F811
    """Test that invalid types error correctly."""
    for bad_type in INVALID_TYPE_ARGS:
        with pytest.raises(SystemExit) as e:
            cli(["-t", bad_type])
            assert e.value.code == 1


def test_invalid_browser(change_homedir):  # noqa: F811
    """Test that invalid browsers error correctly."""
    for bad_browser in INVALID_BROWSER_ARGS:
        with pytest.raises(SystemExit) as e:
            cli(["-b", bad_browser])
            assert e.value.code == 1


def test_chrome_linux_profiles(capsys, become_linux, change_homedir):  # noqa: F811
    """Test --show-profiles option for Chrome on linux"""
    out = None
    try:
        cli(["--show-profiles", "chrome"])
    except SystemExit:
        captured = capsys.readouterr()
        out = captured.out

    assert out.strip() == "Profile"


def test_chromium_linux_profiles(capsys, become_linux, change_homedir):  # noqa: F811
    """Test --show-profiles option for Chromium on linux"""
    out = None
    try:
        cli(["--show-profiles", "chromium"])
    except SystemExit:
        captured = capsys.readouterr()
        out = captured.out
    # use set to make the comparison order-insensitive
    profiles = set(out.strip().split("\n"))
    assert profiles == {"Default", "Profile"}


def test_firefox_windows_profiles(capsys, become_windows, change_homedir):  # noqa: F811
    """Test --show-profiles option for Firefox on Windows"""
    out = None
    try:
        cli(["--show-profiles", "firefox"])
    except SystemExit:
        captured = capsys.readouterr()
        out = captured.out

    assert out.strip() == "Profile 1\nProfile 2"


def test_safari_mac_profiles(caplog, become_mac, change_homedir):  # noqa: F811
    """Test --show-profiles option for Safari on Mac

    This test checks for failure with a error code 1
    """
    try:
        cli(["--show-profiles", "safari"])
    except SystemExit as e:
        assert e.code == 1

    assert len(caplog.records) > 0
    for record in caplog.records:
        assert record.levelname == "CRITICAL"
        assert record.message == "Safari browser does not support profiles"


@pytest.mark.parametrize("platform", all_platform_fixtures, indirect=True)
def test_show_profiles_all(caplog, platform):  # noqa: F811
    """Test --show-profile option with "all" parameter which should fail with
    error code 1.
    """
    try:
        cli(["--show-profiles", "all"])
    except SystemExit as e:
        assert e.code == 1

    assert len(caplog.records) > 0
    for record in caplog.records:
        assert record.levelname == "CRITICAL"
        assert (
            record.message == "'all' cannot be used with --show-profiles, "
            "please specify a single browser"
        )


@pytest.mark.parametrize("browser_arg", INVALID_BROWSER_ARGS)
def test_show_profiles_invalid(caplog, browser_arg):  # noqa: F811
    """Test --show-profile option with "all" parameter which should fail with
    error code 1.
    """
    try:
        cli(["--show-profiles", browser_arg])
    except SystemExit as e:
        assert e.code == 1

    assert len(caplog.records) > 0
    for record in caplog.records:
        assert record.levelname == "ERROR"
        assert record.message.endswith(
            "browser is unavailable. Check --help for available browsers"
        )


@pytest.mark.parametrize("platform", all_platform_fixtures, indirect=True)
@pytest.mark.parametrize("profile_arg", ("-p", "--profile"))
def test_profile_all(capsys, platform, profile_arg):  # noqa: F811
    """Test -p/--profile option with "all" option on all platforms.

    Since the "all" option is not valid, the test checks for exit failure
    """
    try:
        cli([profile_arg, "all"])
    except SystemExit as e:
        assert e.code == 2

    out, err = capsys.readouterr()
    assert out == ""
    print(err)
    assert err.strip().endswith(
        "Cannot use --profile option without specifying"
        " a browser or with --browser set to 'all'"
    )


@pytest.mark.parametrize("profile_arg", ("-p", "--profile"))
def test_profile_safari(caplog, become_mac, profile_arg):  # noqa: F811
    """Test -p/--profile option with Safari on all platforms.

    Since Safari does not support profiles, the test checks for failure
    """
    try:
        cli(["-b", "safari", profile_arg, "some_profile"])
    except SystemExit as e:
        assert e.code == 1

    assert len(caplog.records) > 0
    for record in caplog.records:
        assert record.levelname == "CRITICAL"
        assert record.message == "Safari browser does not support profiles"


@pytest.mark.parametrize("platform", all_platform_fixtures, indirect=True)
@pytest.mark.parametrize("profile_arg", ("-p", "--profile"))
@pytest.mark.parametrize("profile_name", ("nonexistent", "nonexistent2", "some-prof"))
@pytest.mark.parametrize("browser", ("firefox", "chrome"))
def test_profile_nonexistent(
    caplog, platform, profile_arg, profile_name, browser
):  # noqa: F811
    """Test -p/--profile option with a nonexistent profile"""
    try:
        cli(["-b", browser, profile_arg, profile_name])
    except SystemExit as e:
        assert e.code == 1

    assert len(caplog.records) > 0
    for record in caplog.records:
        assert record.levelname == "CRITICAL"
        assert re.match(
            r"Profile '.*' not found in .* browser or profile does not contain history",
            record.message,
        )


def test_firefox_windows_profile(capsys, become_windows, change_homedir):  # noqa: F811
    """Test -p/--profile for Firefox on Windows"""
    cli(["-b", "firefox", "-p", "Profile 2"])

    out, err = capsys.readouterr()
    assert out.startswith("Timestamp,URL")
    assert out.endswith("https://www.reddit.com/\r\n\n")
    assert err == ""


def test_firefox_windows_profile_bookmarks(
    capsys, become_windows, change_homedir  # noqa: F81
):
    """Test -p/--profile for Firefox on Windows, for bookmarks"""
    cli(["-b", "firefox", "-p", "Profile 1", "-t", "bookmarks"])

    out, err = capsys.readouterr()
    assert out.startswith("Timestamp,URL,Title,Folder")
    assert err == ""


@pytest.mark.parametrize("type_arg", ("nonexistent", "cookies", "invalid"))
def test_firefox_windows_profile_invalid_type(caplog, type_arg):  # noqa: F811
    """Test -p/--profile option with a nonexistent profile"""
    try:
        cli(["-b", "firefox", "-p", "Profile 1", "-t", type_arg])
    except SystemExit as e:
        assert e.code == 1

    assert len(caplog.records) > 0
    for record in caplog.records:
        assert record.levelname == "CRITICAL"
        assert re.match(
            r"Type .* is unavailable. Check --help for available types",
            record.message,
        )

<filename>tests/test_cli.py
import csv
import itertools
import json
import tempfile
import re

import pytest

from browser_history.utils import get_browsers
from browser_history.cli import cli, AVAILABLE_BROWSERS
from .utils import (  # noqa: F401
    become_linux,
    become_mac,
    become_windows,
    change_homedir,
)

# pylint: disable=redefined-outer-name,unused-argument

CMD_ROOT = "browser-history"

# Options:
VALID_CMD_OPTS = [
    (f"--{long_o}", f"-{short_o}")
    for long_o, short_o in [
        ("help", "h"),
        ("type", "t"),
        ("browser", "b"),
        ("format", "f"),
        ("output", "o"),
    ]
]
INVALID_CMD_OPTS = [
    "--history",
    "-a",
    "-B",
    "-0",
]

# Arguments, where the default arg should be put first for default testing:
VALID_TYPE_ARGS = [
    "history",
    "bookmarks",
]
INVALID_TYPE_ARGS = ["cookies", "lol", "histories", "bookmark"]
VALID_BROWSER_ARGS = [
    "all",
    "Chrome",
    "Chromium",
    "Firefox",
    "Safari",
    "Edge",
    "Opera",
    "OperaGX",
    "Brave",
]
INVALID_BROWSER_ARGS = ["explorer", "ie", "netscape", "none", "brr"]
VALID_FORMAT_ARGS = [
    "infer",
    "csv",
    "json",
    "jsonl",
]
INVALID_FORMAT_ARGS = ["csvv", "txt", "html", "qwerty"]
# (VALID_OUTPUT_ARGS is any existent file, so validity depends on the fs)
GENERAL_INVALID_ARGS = [
    "foo",
    "0",
]

# Output markers:
HELP_SIGNATURE = (
    "usage: browser-history [-h] [-t TYPE] [-b BROWSER] [-f FORMAT] [-o OUTPUT]"
)
HISTORY_HEADER = "Timestamp,URL"
BOOKMARKS_HEADER = "Timestamp,URL,Title,Folder"

CSV_HISTORY_HEADER = HISTORY_HEADER
CSV_BOOKMARKS_HEADER = BOOKMARKS_HEADER


platform_fixture_map = {
    "linux": become_linux,
    "windows": become_windows,
    "mac": become_mac,
}
all_platform_fixtures = tuple("become_" + plat for plat in platform_fixture_map.keys())


@pytest.fixture
def platform(request):
    """Fixture to change platform based on pytest parameter"""
    request.getfixturevalue("change_homedir")
    return request.getfixturevalue(request.param)


def _get_browser_available_on_system():
    """Find a browser that is supported and installed on the test system."""
    for browser in get_browsers():
        try:  # filter out unsupported browsers
            history = browser().fetch_history()
        except AssertionError:  # anything caught here is unsupported
            continue
        # Has non-empty history so must be installed as well as supported
        if json.loads(history.to_json())["history"]:
            # Return string name which corresponds to a VALID_BROWSER_ARGS entry
            return browser.name
    return  # None indicating no browsers available on system


@pytest.mark.parametrize("platform", all_platform_fixtures, indirect=True)
def test_no_argument(capsys, platform):
    """Test the root command gives basic output."""
    cli([])
    captured = capsys.readouterr()
    assert CSV_HISTORY_HEADER in captured.out


@pytest.mark.parametrize("platform", all_platform_fixtures, indirect=True)
def test_type_argument(capsys, platform):
    """Test arguments for the type option."""
    for type_opt in VALID_CMD_OPTS[1]:
        for type_arg in VALID_TYPE_ARGS:
            cli([type_opt, type_arg])
            captured = capsys.readouterr()
            if type_arg == "history":
                # assuming csv is default
                assert captured.out.startswith(CSV_HISTORY_HEADER)
            if type_arg == "bookmarks":
                assert captured.out.startswith(CSV_BOOKMARKS_HEADER)


@pytest.mark.parametrize("browser_arg", VALID_BROWSER_ARGS)
@pytest.mark.parametrize("platform", all_platform_fixtures, indirect=True)
def test_browser_argument(browser_arg, capsys, platform):
    """Test arguments for the browser option."""
    for browser_opt in VALID_CMD_OPTS[2]:
        try:
            cli([browser_opt, browser_arg])
            captured = capsys.readouterr()
            assert CSV_HISTORY_HEADER in captured.out
        except AssertionError as e:
            if any(
                browser_unavailable_err in e.args[0]
                for browser_unavailable_err in (
                    "browser is not supported",
                    "browser is not installed",
                )
            ):
                # In case the tester does not have access to the browser
                # in question, which makes the command fail, but in a way
                # that is expected and gives a recognised error message.
                pytest.skip(
                    "Unable to test against {} browser because it is "
                    "not available locally".format(browser_opt)
                )
            else:  # command fails for another reason i.e. a test failure
                pytest.fail(
                    "{} browser is available but the command to fetch "
                    "the history from it has failed.".format(browser_opt)
                )


@pytest.mark.parametrize("platform", all_platform_fixtures, indirect=True)
def test_format_argument(capsys, platform):
    """Tests arguments for the format option."""
    # First check format of default:
    cli([])
    captured = capsys.readouterr()
    csv_output = captured.out
    # Sniffer determines format, less intensive than reading in csv.reader
    # and we don't mind the CSV dialect, so just check call doesn't error
    read_csv = csv.Sniffer()
    # This gives '_csv.Error: Could not determine delimiter' if not a csv file
    read_csv.sniff(csv_output, delimiters=",")
    assert read_csv.has_header(
        csv_output
    ), "CSV format missing heading with type followed by column names."

    for fmt_opt in VALID_CMD_OPTS[3]:
        for fmt_arg in VALID_FORMAT_ARGS:
            cli([fmt_opt, fmt_arg])
            output = capsys.readouterr().out
            if fmt_arg in ("csv", "infer"):  # infer gives csv if no file
                read_csv.sniff(output, delimiters=",")
                assert read_csv.has_header(output)
                assert CSV_HISTORY_HEADER in output
            elif fmt_arg == "json":
                json.loads(output)
            elif fmt_arg == "jsonl":
                # Newline-delimited json so test each line is valid json
                for line in output.splitlines():
                    json.loads(line)


@pytest.mark.parametrize("platform", all_platform_fixtures, indirect=True)
def test_output_argument(capsys, platform):
    """Test arguments for the output option."""
    for output_opt in VALID_CMD_OPTS[4]:
        with tempfile.TemporaryDirectory() as tmpdir:
            cli([output_opt, tmpdir + "out.csv"])
            output = capsys.readouterr().out
            # Check output was not sent to STDOUT since should go to file
            assert HISTORY_HEADER not in output
            with open(tmpdir + "out.csv", "rt") as f:  # now check the file
                assert HISTORY_HEADER in f.read()


@pytest.mark.parametrize("platform", all_platform_fixtures, indirect=True)
@pytest.mark.parametrize("browser", AVAILABLE_BROWSERS.split(", "))
def test_argument_combinations(capsys, platform, browser):
    """Test that combinations of optional arguments work properly."""
    # Choose some representative combinations. No need to test every one.
    indices = (0, 1)
    available_browser = _get_browser_available_on_system()
    # To test combos of short or long option variants
    for index_a, index_b in itertools.product(indices, indices):
        if available_browser:
            try:
                cli(
                    [
                        VALID_CMD_OPTS[1][index_a],  # type
                        VALID_TYPE_ARGS[1],  # ... is bookmarks,
                        VALID_CMD_OPTS[2][index_a],  # browser
                        browser,  # ... is any usable on system
                    ]
                )
            except AssertionError as e:
                if any(
                    browser_unavailable_err in e.args[0]
                    for browser_unavailable_err in (
                        "browser is not supported",
                        "browser is not installed",
                        "Bookmarks are not supported",
                    )
                ):
                    # In case the tester does not have access to the browser
                    # in question, which makes the command fail, but in a way
                    # that is expected and gives a recognised error message.
                    pytest.skip(
                        "Unable to test against {} browser because it is "
                        "not available locally".format(browser)
                    )
                else:  # command fails for another reason i.e. a test failure
                    pytest.fail(
                        "{} browser is available but the command to fetch "
                        "the history from it has failed.".format(browser)
                    )
            output = capsys.readouterr().out

            read_csv = csv.Sniffer()
            read_csv.sniff(output, delimiters=",")
            assert read_csv.has_header(output)
        else:  # unlikely but catch just in case can't use any browser
            for _ in range(3):  # to cover all three assert tests above
                pytest.skip("No browsers available to test with")
        with tempfile.TemporaryDirectory() as tmpdir:
            # This command should write to the given output file:
            cli(
                [
                    VALID_CMD_OPTS[3][index_b],  # format
                    VALID_FORMAT_ARGS[2],  # ... is json,
                    VALID_CMD_OPTS[4][index_b],  # output
                    tmpdir + "out.json",  # ... is a named file
                ]
            )
            with open(tmpdir + "out.json", "rb") as f:
                json.loads(f.read().decode("utf-8"))


def test_help_option(capsys):
    """Test the command-line help provided to the user on request."""
    for help_opt in VALID_CMD_OPTS[0]:
        with pytest.raises(SystemExit) as e:
            cli([help_opt])
            output = capsys.readouterr()
            assert HELP_SIGNATURE in output
            assert e.value.code == 0


def test_invalid_options(change_homedir):  # noqa: F811
    """Test that invalid options error correctly."""
    for bad_opt in INVALID_CMD_OPTS:
        with pytest.raises(SystemExit) as e:
            cli([bad_opt])
            assert e.value.code == 2


def test_invalid_format(change_homedir):  # noqa: F811
    """Test that invalid formats error correctly."""
    for bad_format in INVALID_FORMAT_ARGS:
        with pytest.raises(SystemExit) as e:
            cli(["-f", bad_format])
            assert e.value.code == 1


def test_invalid_type(change_homedir):  # noqa: F811
    """Test that invalid types error correctly."""
    for bad_type in INVALID_TYPE_ARGS:
        with pytest.raises(SystemExit) as e:
            cli(["-t", bad_type])
            assert e.value.code == 1


def test_invalid_browser(change_homedir):  # noqa: F811
    """Test that invalid browsers error correctly."""
    for bad_browser in INVALID_BROWSER_ARGS:
        with pytest.raises(SystemExit) as e:
            cli(["-b", bad_browser])
            assert e.value.code == 1


def test_chrome_linux_profiles(capsys, become_linux, change_homedir):  # noqa: F811
    """Test --show-profiles option for Chrome on linux"""
    out = None
    try:
        cli(["--show-profiles", "chrome"])
    except SystemExit:
        captured = capsys.readouterr()
        out = captured.out

    assert out.strip() == "Profile"


def test_chromium_linux_profiles(capsys, become_linux, change_homedir):  # noqa: F811
    """Test --show-profiles option for Chromium on linux"""
    out = None
    try:
        cli(["--show-profiles", "chromium"])
    except SystemExit:
        captured = capsys.readouterr()
        out = captured.out
    # use set to make the comparison order-insensitive
    profiles = set(out.strip().split("\n"))
    assert profiles == {"Default", "Profile"}


def test_firefox_windows_profiles(capsys, become_windows, change_homedir):  # noqa: F811
    """Test --show-profiles option for Firefox on Windows"""
    out = None
    try:
        cli(["--show-profiles", "firefox"])
    except SystemExit:
        captured = capsys.readouterr()
        out = captured.out

    assert out.strip() == "Profile 1\nProfile 2"


def test_safari_mac_profiles(caplog, become_mac, change_homedir):  # noqa: F811
    """Test --show-profiles option for Safari on Mac

    This test checks for failure with a error code 1
    """
    try:
        cli(["--show-profiles", "safari"])
    except SystemExit as e:
        assert e.code == 1

    assert len(caplog.records) > 0
    for record in caplog.records:
        assert record.levelname == "CRITICAL"
        assert record.message == "Safari browser does not support profiles"


@pytest.mark.parametrize("platform", all_platform_fixtures, indirect=True)
def test_show_profiles_all(caplog, platform):  # noqa: F811
    """Test --show-profile option with "all" parameter which should fail with
    error code 1.
    """
    try:
        cli(["--show-profiles", "all"])
    except SystemExit as e:
        assert e.code == 1

    assert len(caplog.records) > 0
    for record in caplog.records:
        assert record.levelname == "CRITICAL"
        assert (
            record.message == "'all' cannot be used with --show-profiles, "
            "please specify a single browser"
        )


@pytest.mark.parametrize("browser_arg", INVALID_BROWSER_ARGS)
def test_show_profiles_invalid(caplog, browser_arg):  # noqa: F811
    """Test --show-profile option with "all" parameter which should fail with
    error code 1.
    """
    try:
        cli(["--show-profiles", browser_arg])
    except SystemExit as e:
        assert e.code == 1

    assert len(caplog.records) > 0
    for record in caplog.records:
        assert record.levelname == "ERROR"
        assert record.message.endswith(
            "browser is unavailable. Check --help for available browsers"
        )


@pytest.mark.parametrize("platform", all_platform_fixtures, indirect=True)
@pytest.mark.parametrize("profile_arg", ("-p", "--profile"))
def test_profile_all(capsys, platform, profile_arg):  # noqa: F811
    """Test -p/--profile option with "all" option on all platforms.

    Since the "all" option is not valid, the test checks for exit failure
    """
    try:
        cli([profile_arg, "all"])
    except SystemExit as e:
        assert e.code == 2

    out, err = capsys.readouterr()
    assert out == ""
    print(err)
    assert err.strip().endswith(
        "Cannot use --profile option without specifying"
        " a browser or with --browser set to 'all'"
    )


@pytest.mark.parametrize("profile_arg", ("-p", "--profile"))
def test_profile_safari(caplog, become_mac, profile_arg):  # noqa: F811
    """Test -p/--profile option with Safari on all platforms.

    Since Safari does not support profiles, the test checks for failure
    """
    try:
        cli(["-b", "safari", profile_arg, "some_profile"])
    except SystemExit as e:
        assert e.code == 1

    assert len(caplog.records) > 0
    for record in caplog.records:
        assert record.levelname == "CRITICAL"
        assert record.message == "Safari browser does not support profiles"


@pytest.mark.parametrize("platform", all_platform_fixtures, indirect=True)
@pytest.mark.parametrize("profile_arg", ("-p", "--profile"))
@pytest.mark.parametrize("profile_name", ("nonexistent", "nonexistent2", "some-prof"))
@pytest.mark.parametrize("browser", ("firefox", "chrome"))
def test_profile_nonexistent(
    caplog, platform, profile_arg, profile_name, browser
):  # noqa: F811
    """Test -p/--profile option with a nonexistent profile"""
    try:
        cli(["-b", browser, profile_arg, profile_name])
    except SystemExit as e:
        assert e.code == 1

    assert len(caplog.records) > 0
    for record in caplog.records:
        assert record.levelname == "CRITICAL"
        assert re.match(
            r"Profile '.*' not found in .* browser or profile does not contain history",
            record.message,
        )


def test_firefox_windows_profile(capsys, become_windows, change_homedir):  # noqa: F811
    """Test -p/--profile for Firefox on Windows"""
    cli(["-b", "firefox", "-p", "Profile 2"])

    out, err = capsys.readouterr()
    assert out.startswith("Timestamp,URL")
    assert out.endswith("https://www.reddit.com/\r\n\n")
    assert err == ""


def test_firefox_windows_profile_bookmarks(
    capsys, become_windows, change_homedir  # noqa: F81
):
    """Test -p/--profile for Firefox on Windows, for bookmarks"""
    cli(["-b", "firefox", "-p", "Profile 1", "-t", "bookmarks"])

    out, err = capsys.readouterr()
    assert out.startswith("Timestamp,URL,Title,Folder")
    assert err == ""


@pytest.mark.parametrize("type_arg", ("nonexistent", "cookies", "invalid"))
def test_firefox_windows_profile_invalid_type(caplog, type_arg):  # noqa: F811
    """Test -p/--profile option with a nonexistent profile"""
    try:
        cli(["-b", "firefox", "-p", "Profile 1", "-t", type_arg])
    except SystemExit as e:
        assert e.code == 1

    assert len(caplog.records) > 0
    for record in caplog.records:
        assert record.levelname == "CRITICAL"
        assert re.match(
            r"Type .* is unavailable. Check --help for available types",
            record.message,
        )

# noqa: F401 # pylint: disable=redefined-outer-name,unused-argument # Options: # Arguments, where the default arg should be put first for default testing: # (VALID_OUTPUT_ARGS is any existent file, so validity depends on the fs) # Output markers: Fixture to change platform based on pytest parameter Find a browser that is supported and installed on the test system. # filter out unsupported browsers # anything caught here is unsupported # Has non-empty history so must be installed as well as supported # Return string name which corresponds to a VALID_BROWSER_ARGS entry # None indicating no browsers available on system Test the root command gives basic output. Test arguments for the type option. # assuming csv is default Test arguments for the browser option. # In case the tester does not have access to the browser # in question, which makes the command fail, but in a way # that is expected and gives a recognised error message. # command fails for another reason i.e. a test failure Tests arguments for the format option. # First check format of default: # Sniffer determines format, less intensive than reading in csv.reader # and we don't mind the CSV dialect, so just check call doesn't error # This gives '_csv.Error: Could not determine delimiter' if not a csv file # infer gives csv if no file # Newline-delimited json so test each line is valid json Test arguments for the output option. # Check output was not sent to STDOUT since should go to file # now check the file Test that combinations of optional arguments work properly. # Choose some representative combinations. No need to test every one. # To test combos of short or long option variants # type # ... is bookmarks, # browser # ... is any usable on system # In case the tester does not have access to the browser # in question, which makes the command fail, but in a way # that is expected and gives a recognised error message. # command fails for another reason i.e. a test failure # unlikely but catch just in case can't use any browser # to cover all three assert tests above # This command should write to the given output file: # format # ... is json, # output # ... is a named file Test the command-line help provided to the user on request. # noqa: F811 Test that invalid options error correctly. # noqa: F811 Test that invalid formats error correctly. # noqa: F811 Test that invalid types error correctly. # noqa: F811 Test that invalid browsers error correctly. # noqa: F811 Test --show-profiles option for Chrome on linux # noqa: F811 Test --show-profiles option for Chromium on linux # use set to make the comparison order-insensitive # noqa: F811 Test --show-profiles option for Firefox on Windows # noqa: F811 Test --show-profiles option for Safari on Mac      This test checks for failure with a error code 1 # noqa: F811 Test --show-profile option with "all" parameter which should fail with     error code 1. # noqa: F811 Test --show-profile option with "all" parameter which should fail with     error code 1. # noqa: F811 Test -p/--profile option with "all" option on all platforms.      Since the "all" option is not valid, the test checks for exit failure # noqa: F811 Test -p/--profile option with Safari on all platforms.      Since Safari does not support profiles, the test checks for failure # noqa: F811 Test -p/--profile option with a nonexistent profile # noqa: F811 Test -p/--profile for Firefox on Windows # noqa: F81 Test -p/--profile for Firefox on Windows, for bookmarks # noqa: F811 Test -p/--profile option with a nonexistent profile