#
# Copyright (C) 2023 Red Hat, Inc.
# SPDX-License-Identifier: GPL-3.0-or-later


import json
from pathlib import Path

import pytest

from cockpit.jsonutil import JsonValue
from cockpit.packages import Packages, PackagesLoader, parse_accept_language


@pytest.mark.parametrize(("test_input", "expected"), [
    # correct handles empty values
    ('', ()),
    ('  ', ()),
    (' , ', ()),
    (' , ,xx', ('xx',)),
    # english → empty list
    ('en', ()),
    ('   , en', ()),
    # invalid q values get ignored
    ('aa;q===,bb;q=abc,cc;q=.,zz', ('zz',)),
    # variant-peeling works
    ('aa-bb-cc-dd,ee-ff-gg-hh', ('aa-bb-cc-dd', 'aa-bb-cc', 'aa-bb', 'aa', 'ee-ff-gg-hh', 'ee-ff-gg', 'ee-ff', 'ee')),
    # sorting and english-truncation are working
    ('fr-ch;q=0.8,es-mx;q=1.0,en-ca;q=0.9', ('es-mx', 'es', 'en-ca')),
    ('de-at, zh-CN, en,', ('de-at', 'de', 'zh-cn', 'zh')),
    ('es-es, nl;q=0.8, fr;q=0.9', ('es-es', 'es', 'fr', 'nl')),
    ('fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5', ('fr-ch', 'fr'))
])
def test_parse_accept_language(test_input: str, expected: 'tuple[str]') -> None:
    assert parse_accept_language(test_input) == expected


@pytest.fixture
def pkgdir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
    monkeypatch.setenv('XDG_DATA_DIRS', str(tmp_path))
    monkeypatch.setenv('XDG_DATA_HOME', '/nonexisting')

    self = tmp_path / 'cockpit'
    self.mkdir()
    make_package(self, 'basic', description="standard package", requires={"cockpit": "42"})
    return self


@pytest.fixture
def confdir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
    monkeypatch.setenv('XDG_CONFIG_DIRS', str(tmp_path))
    return tmp_path / 'cockpit'


def make_package(pkgdir: Path, dirname: str, **kwargs: JsonValue) -> None:
    (pkgdir / dirname).mkdir()
    with (pkgdir / dirname / 'manifest.json').open('w') as file:
        json.dump(kwargs, file, indent=2)


def test_basic(pkgdir: Path) -> None:
    packages = Packages()
    assert len(packages.packages) == 1
    assert packages.packages['basic'].name == 'basic'
    assert packages.packages['basic'].manifest['description'] == 'standard package'
    assert packages.packages['basic'].manifest['requires'] == {'cockpit': "42"}
    assert packages.packages['basic'].priority == 1

    assert packages.manifests == '{"basic": {"description": "standard package", "requires": {"cockpit": "42"}}}'


def test_override_etc(pkgdir: Path, confdir: Path) -> None:
    (confdir / 'basic.override.json').write_text('{"description": null, "priority": 5, "does-not-exist": null}')

    packages = Packages()
    assert len(packages.packages) == 1
    # original attributes
    assert packages.packages['basic'].name == 'basic'
    assert packages.packages['basic'].manifest['requires'] == {'cockpit': '42'}
    # overridden attributes
    assert 'description' not in packages.packages['basic'].manifest
    assert packages.packages['basic'].priority == 5

    assert json.loads(packages.manifests) == {
        'basic': {
            'requires': {'cockpit': '42'},
            'priority': 5,
        }
    }


def test_priority(pkgdir: Path) -> None:
    make_package(pkgdir, 'vip', name='basic', description='VIP', priority=100)
    make_package(pkgdir, 'guest', description='Guest')

    packages = Packages()
    assert len(packages.packages) == 2
    assert packages.packages['basic'].name == 'basic'
    assert packages.packages['basic'].priority == 100
    assert packages.packages['basic'].manifest['description'] == 'VIP'
    assert packages.packages['guest'].name == 'guest'
    assert packages.packages['guest'].priority == 1

    parsed = json.loads(packages.manifests)
    assert parsed['basic'] == {'name': 'basic', 'description': 'VIP', 'priority': 100}
    assert parsed['guest'] == {'description': 'Guest'}


def test_conditions(pkgdir: Path) -> None:
    make_package(pkgdir, 'empty', conditions=[])

    # path-exists only
    make_package(pkgdir, 'exists-1-yes', conditions=[{'path-exists': '/usr'}])
    make_package(pkgdir, 'exists-1-no', conditions=[{'path-exists': '/nonexisting'}])
    make_package(pkgdir, 'exists-2-yes', conditions=[{"path-exists": "/usr"},
                                                     {"path-exists": "/bin/sh"}])
    make_package(pkgdir, 'exists-2-no', conditions=[{"path-exists": "/usr"},
                                                    {"path-exists": "/nonexisting"}])

    # path-not-exists only
    make_package(pkgdir, 'notexists-1-yes', conditions=[{"path-not-exists": "/nonexisting"}])
    make_package(pkgdir, 'notexists-1-no', conditions=[{"path-not-exists": "/usr"}])
    make_package(pkgdir, 'notexists-2-yes', conditions=[{"path-not-exists": "/nonexisting"},
                                                        {"path-not-exists": "/obscure"}])
    make_package(pkgdir, 'notexists-2-no', conditions=[{"path-not-exists": "/nonexisting"},
                                                       {"path-not-exists": "/usr"}])

    # mixed
    make_package(pkgdir, 'mixed-yes', conditions=[{"path-exists": "/usr"},
                                                  {"path-not-exists": "/nonexisting"}])
    make_package(pkgdir, 'mixed-no', conditions=[{"path-exists": "/nonexisting"},
                                                 {"path-not-exists": "/obscure"}])

    packages = Packages()
    assert set(packages.packages.keys()) == {
        'basic', 'empty', 'exists-1-yes', 'exists-2-yes', 'notexists-1-yes', 'notexists-2-yes', 'mixed-yes'
    }

    # Manifest.get_condition_files()
    assert set(packages.packages['empty'].manifest.get_condition_files()) == set()
    assert set(packages.packages['exists-1-yes'].manifest.get_condition_files()) == {'/usr'}
    assert set(packages.packages['exists-2-yes'].manifest.get_condition_files()) == {'/usr', '/bin/sh'}
    assert set(packages.packages['mixed-yes'].manifest.get_condition_files()) == {'/usr', '/nonexisting'}

    # PackagesLoader.get_condition_files()
    assert set(PackagesLoader.get_condition_files()) == {'/usr', '/bin/sh', '/nonexisting', '/obscure'}


def test_conditions_errors(pkgdir: Path) -> None:
    make_package(pkgdir, 'broken-syntax-1', conditions=[1])
    make_package(pkgdir, 'broken-syntax-2', conditions=[["path-exists"]])
    make_package(pkgdir, 'broken-syntax-3', conditions=[{"path-exists": "/foo", "path-not-exists": "/bar"}])

    make_package(pkgdir, 'unknown-predicate-good', conditions=[{"path-exists": "/usr"},
                                                               {"frobnicated": True}])
    make_package(pkgdir, 'unknown-predicate-bad', conditions=[{"path-exists": "/nonexisting"},
                                                              {"frobnicated": True}])

    packages = Packages()
    assert set(packages.packages.keys()) == {'basic', 'unknown-predicate-good'}

    # get_condition_files() should have the valid ones
    assert set(packages.packages['unknown-predicate-good'].manifest.get_condition_files()) == {'/usr'}

    assert set(PackagesLoader.get_condition_files()) == {'/usr', '/nonexisting'}


def test_condition_hides_priority(pkgdir: Path) -> None:
    make_package(pkgdir, 'vip', name="basic", description="VIP", priority=100,
                 conditions=[{"path-exists": "/nonexisting"}])

    packages = Packages()
    assert packages.packages['basic'].name == 'basic'
    assert packages.packages['basic'].manifest['description'] == 'standard package'
    assert packages.packages['basic'].manifest['requires'] == {'cockpit': "42"}
    assert packages.packages['basic'].priority == 1


def test_conditions_any(pkgdir: Path) -> None:
    make_package(pkgdir, 'any-empty-fail', conditions=[{"any": []}])

    # Test basic "any" condition - should pass if any one condition is true
    make_package(pkgdir, 'any-pass-1', conditions=[
        {"any": [{"path-exists": "/usr"}, {"path-exists": "/nonexisting"}]}
    ])
    make_package(pkgdir, 'any-pass-2', conditions=[
        {"any": [{"path-exists": "/nonexisting"}, {"path-exists": "/usr"}]}
    ])
    make_package(pkgdir, 'any-fail', conditions=[
        {"any": [{"path-exists": "/nonexisting"}, {"path-exists": "/alsonotexisting"}]}
    ])

    # Test "any" mixed with path-not-exists
    make_package(pkgdir, 'any-mixed-pass', conditions=[
        {"any": [{"path-not-exists": "/nonexisting"}, {"path-exists": "/alsonotexisting"}]}
    ])
    make_package(pkgdir, 'any-mixed-fail', conditions=[
        {"any": [{"path-not-exists": "/usr"}, {"path-exists": "/nonexisting"}]}
    ])

    # Test combination of regular conditions with "any" conditions
    make_package(pkgdir, 'mixed-any-pass', conditions=[
        {"any": [{"path-exists": "/usr"}, {"path-exists": "/nonexisting"}]},
        {"path-not-exists": "/nonexisting"}
    ])
    make_package(pkgdir, 'mixed-any-fail-1', conditions=[
        {"any": [{"path-exists": "/nonexisting"}, {"path-exists": "/alsonotexisting"}]},
        {"path-not-exists": "/nonexisting"}
    ])
    make_package(pkgdir, 'mixed-any-fail-2', conditions=[
        {"any": [{"path-exists": "/usr"}, {"path-exists": "/nonexisting"}]},
        {"path-not-exists": "/usr"}
    ])

    # Test nested "any" conditions
    make_package(pkgdir, 'nested-any-pass', conditions=[
        {"any": [
            {"any": [{"path-exists": "/usr"}, {"path-exists": "/nonexisting2"}]},
            {"path-not-exists": "/nonexisting3"}
        ]}
    ])
    make_package(pkgdir, 'nested-any-fail', conditions=[
        {"any": [
            {"any": [{"path-exists": "/nonexisting"}, {"path-exists": "/alsonotexisting"}]},
            {"path-not-exists": "/usr"}
        ]}
    ])

    packages = Packages()
    assert set(packages.packages.keys()) == {
        'basic', 'any-pass-1', 'any-pass-2', 'any-mixed-pass', 'mixed-any-pass', 'nested-any-pass',
    }

    # get_condition_files()
    assert set(packages.packages['any-pass-1'].manifest.get_condition_files()) == {'/usr', '/nonexisting'}
    assert set(packages.packages['mixed-any-pass'].manifest.get_condition_files()) == {'/usr', '/nonexisting'}
    assert set(packages.packages['nested-any-pass'].manifest.get_condition_files()) == {
        '/usr', '/nonexisting2', '/nonexisting3'
    }

    assert set(PackagesLoader.get_condition_files()) == {'/usr', '/nonexisting', '/nonexisting2',
                                                          '/nonexisting3', '/alsonotexisting'}


def test_conditions_any_errors(pkgdir: Path) -> None:
    # Test "any" with invalid syntax
    make_package(pkgdir, 'any-invalid-1', conditions=[{"any": "not-a-list"}])
    make_package(pkgdir, 'any-invalid-2', conditions=[{"any": [1, 2, 3]}])
    make_package(pkgdir, 'any-invalid-3', conditions=[{"any": [{"invalid": "dict", "multiple": "keys"}]}])

    # Unknown predicates inside "any" are ignored
    make_package(pkgdir, 'any-mixed-unknown', conditions=[
        {"any": [{"path-exists": "/usr"}, {"frobnicated": True}]}
    ])

    packages = Packages()
    assert set(packages.packages.keys()) == {'basic', 'any-mixed-unknown'}


def test_english_translation(pkgdir: Path) -> None:
    make_package(pkgdir, 'one')
    (pkgdir / 'one' / 'po.de.js').write_text('eins')

    packages = Packages()

    # make sure we get German
    document = packages.load_path('/one/po.js', {'Accept-Language': 'de'})
    assert '/javascript' in document.content_type
    assert document.data.read() == b'eins'

    # make sure we get German here (higher q-value) even with English first
    document = packages.load_path('/one/po.js', {'Accept-Language': 'en;q=0.9, de-ch'})
    assert '/javascript' in document.content_type
    assert document.data.read() == b'eins'

    # make sure we get the empty ("English") translation, and not German
    document = packages.load_path('/one/po.js', {'Accept-Language': 'en, de'})
    assert '/javascript' in document.content_type
    assert document.data.read() == b''

    document = packages.load_path('/one/po.js', {'Accept-Language': 'de;q=0.9, fr;q=0.7, en'})
    assert '/javascript' in document.content_type
    assert document.data.read() == b''

    document = packages.load_path('/one/po.js', {'Accept-Language': 'de;q=0.9, fr, en-ca'})
    assert '/javascript' in document.content_type
    assert document.data.read() == b''

    document = packages.load_path('/one/po.js', {'Accept-Language': ''})
    assert '/javascript' in document.content_type
    assert document.data.read() == b''

    document = packages.load_path('/one/po.js', {})
    assert '/javascript' in document.content_type
    assert document.data.read() == b''


def test_translation(pkgdir: Path) -> None:
    # old style: make sure po.de.js is served as fallback for manifest translations
    make_package(pkgdir, 'one')
    (pkgdir / 'one' / 'po.de.js').write_text('eins')

    # new style: separated translations
    make_package(pkgdir, 'two')
    (pkgdir / 'two' / 'po.de.js').write_text('zwei')
    (pkgdir / 'two' / 'po.manifest.de.js').write_text('zwo')

    packages = Packages()

    # make sure we can read a po.js file with language fallback
    document = packages.load_path('/one/po.js', {'Accept-Language': 'es, de'})
    assert '/javascript' in document.content_type
    assert document.data.read() == b'eins'

    # make sure we fall back cleanly to an empty file with correct mime
    document = packages.load_path('/one/po.js', {'Accept-Language': 'es'})
    assert '/javascript' in document.content_type
    assert document.data.read() == b''

    # make sure the manifest translations get sent along with manifests.js
    document = packages.load_path('/manifests-i18n.js', {'Accept-Language': 'de'})
    contents = document.data.read()
    assert b'eins\n' in contents
    assert b'zwo\n' in contents
    assert b'zwei\n' not in contents


def test_filename_mangling(pkgdir: Path) -> None:
    make_package(pkgdir, 'one')

    # test various filename variations
    (pkgdir / 'one' / 'one.js').write_text('this is one.js')
    (pkgdir / 'one' / 'two.js.gz').write_text('this is two.js')
    (pkgdir / 'one' / 'three.min.js.gz').write_text('this is three.js')
    (pkgdir / 'one' / 'four.min.js').write_text('this is four.js')

    packages = Packages()
    encodings = set()

    for name in ['one', 'two', 'three', 'four']:
        document = packages.load_path(f'/one/{name}.js', {})
        assert document.data.read().decode() == f'this is {name}.js'
        assert '/javascript' in document.content_type
        encodings.add(document.content_encoding)

    assert encodings == {None, 'gzip'}  # make sure we saw both compressed and uncompressed


def test_overlapping_minified(pkgdir: Path) -> None:
    make_package(pkgdir, 'one')
    (pkgdir / 'one' / 'one.min.js').write_text('min')
    (pkgdir / 'one' / 'one.js').write_text('max')

    # try the other way around in hope of listing the files in reverse order
    (pkgdir / 'one' / 'two.js').write_text('max')
    (pkgdir / 'one' / 'two.min.js').write_text('min')

    packages = Packages()

    # if both files are present, we should find the original one
    document = packages.load_path('/one/one.js', {})
    assert document.data.read().decode() == 'max'
    document = packages.load_path('/one/two.js', {})
    assert document.data.read().decode() == 'max'

    # but requesting .min. explicitly will load it
    document = packages.load_path('/one/one.min.js', {})
    assert document.data.read().decode() == 'min'
    document = packages.load_path('/one/two.min.js', {})
    assert document.data.read().decode() == 'min'
