#
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
import datetime
import pytest

from dataclasses import replace
from pathlib import Path
from pytest_mock import MockerFixture
from sqlite3 import Cursor
from typing import Any, TypeVar
from unittest.mock import MagicMock

from ahriman.application.ahriman import _parser
from ahriman.core.alpm.remote import AUR
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.database.migrations import Migrations
from ahriman.core.log.log_loader import LogLoader
from ahriman.core.repository import Repository
from ahriman.core.repository.package_info import PackageInfo
from ahriman.core.sign.gpg import GPG
from ahriman.core.spawn import Spawn
from ahriman.core.status import Client
from ahriman.core.status.event_bus import EventBus
from ahriman.core.status.watcher import Watcher
from ahriman.models.aur_package import AURPackage
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.migration import Migration
from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription
from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
from ahriman.models.result import Result
from ahriman.models.user import User
from ahriman.models.user_access import UserAccess


T = TypeVar("T")


# helpers
# https://stackoverflow.com/a/21611963
def anyvar(cls: type[T], strict: bool = False) -> T:
    """
    any value helper for mocker calls check

    Args:
        cls(type[T]): type of the variable to check
        strict(bool, optional): if True then check type of supplied argument (Default value = False)

    Returns:
        T: any wrapper
    """
    class AnyVar(cls):
        """
        any value wrapper
        """

        def __eq__(self, other: Any) -> bool:
            """
            compare object to other

            Args:
                other(Any): other object to compare

            Returns:
                bool: True in case if objects are equal
            """
            return not strict or isinstance(other, cls)

    return AnyVar()


def get_package_status(package: Package) -> dict[str, Any]:
    """
    helper to extract package status from package

    Args:
        package(Package): package object

    Returns:
        dict[str, Any]: simplified package status map (with only status and view)
    """
    return {"status": BuildStatusEnum.Unknown.value, "package": package.view()}


def get_package_status_extended(package: Package) -> dict[str, Any]:
    """
    helper to extract package status from package

    Args:
        package(Package): package object

    Returns:
        dict[str, Any]: full package status map (with timestamped build status and view)
    """
    return {"status": BuildStatus().view(), "package": package.view()}


@pytest.hookimpl(trylast=True)
def pytest_configure() -> None:
    """
    register helpers after pytest-helpers-namespace has initialized
    """
    for helper in (anyvar, get_package_status, get_package_status_extended):
        pytest.helpers.register(helper)


# generic fixtures
@pytest.fixture(autouse=True)
def _register_log_context() -> None:
    """
    register log context variables and factory
    """
    LogLoader.register_context()


@pytest.fixture
def args() -> argparse.Namespace:
    """
    fixture for command line arguments

    Returns:
        argparse.Namespace: command line arguments test instance
    """
    return argparse.Namespace(architecture=None, lock=None, force=False, unsafe=False, report=False,
                              repository=None, repository_id=None, wait_timeout=-1)


@pytest.fixture
def aur_package_ahriman() -> AURPackage:
    """
    fixture for AUR package

    Returns:
        AURPackage: AUR package test instance
    """
    return AURPackage(
        id=1197565,
        name="ahriman",
        package_base_id=165427,
        package_base="ahriman",
        version="2.6.0-1",
        description="ArcH linux ReposItory MANager",
        num_votes=0,
        popularity=0,
        first_submitted=datetime.datetime.fromtimestamp(1618008285, datetime.UTC),
        last_modified=datetime.datetime.fromtimestamp(1673826351, datetime.UTC),
        url_path="/cgit/aur.git/snapshot/ahriman.tar.gz",
        url="https://github.com/arcan1s/ahriman",
        out_of_date=None,
        maintainer="arcanis",
        submitter="arcanis",
        depends=[
            "devtools",
            "git",
            "pyalpm",
            "python-cerberus",
            "python-inflection",
            "python-passlib",
            "python-requests",
            "python-setuptools",
            "python-srcinfo",
        ],
        make_depends=[
            "python-build",
            "python-installer",
            "python-wheel",
        ],
        opt_depends=[
            "breezy",
            "darcs",
            "mercurial",
            "python-aioauth-client",
            "python-aiohttp",
            "python-aiohttp-debugtoolbar",
            "python-aiohttp-jinja2",
            "python-aiohttp-security",
            "python-aiohttp-session",
            "python-boto3",
            "python-cryptography",
            "python-requests-unixsocket",
            "python-jinja",
            "rsync",
            "subversion",
        ],
        check_depends=[
            "python-pytest",
        ],
        conflicts=[],
        provides=[],
        license=["GPL3"],
        keywords=[],
        groups=[],
    )


@pytest.fixture
def configuration(repository_id: RepositoryId, tmp_path: Path, resource_path_root: Path) -> Configuration:
    """
    configuration fixture

    Args:
        repository_id(RepositoryId): repository identifier fixture
        tmp_path(Path): temporary path used by the fixture as root
        resource_path_root(Path): resource path root directory

    Returns:
        Configuration: configuration test instance
    """
    path = resource_path_root / "core" / "ahriman.ini"

    instance = Configuration.from_path(path, repository_id)
    instance.set_option("repository", "root", str(tmp_path))
    instance.set_option("settings", "database", str(tmp_path / "ahriman.db"))

    return instance


@pytest.fixture
def database(configuration: Configuration, mocker: MockerFixture) -> SQLite:
    """
    database fixture

    Args:
        configuration(Configuration): configuration fixture
        mocker(MockerFixture): mocker object

    Returns:
        SQLite: database test instance
    """
    original_method = Migrations.perform_migration

    def perform_migration(self: Migrations, cursor: Cursor, migration: Migration) -> None:
        original_method(self, cursor, replace(migration, migrate_data=lambda *args: None))

    mocker.patch.object(Migrations, "perform_migration", autospec=True, side_effect=perform_migration)
    return SQLite.load(configuration)


@pytest.fixture
def gpg(configuration: Configuration) -> GPG:
    """
    fixture for empty GPG

    Args:
        configuration(Configuration): configuration fixture

    Returns:
        GPG: GPG test instance
    """
    return GPG(configuration)


@pytest.fixture
def local_client(database: SQLite, configuration: Configuration) -> Client:
    """
    local status client

    Args:
        database(SQLite): database fixture
        configuration(Configuration): configuration fixture

    Returns:
        Client: local status client test instance
    """
    _, repository_id = configuration.check_loaded()
    return Client.load(repository_id, configuration, database, report=False)


@pytest.fixture
def package_ahriman(package_description_ahriman: PackageDescription, remote_source: RemoteSource) -> Package:
    """
    package fixture

    Args:
        package_description_ahriman(PackageDescription): description fixture
        remote_source(RemoteSource): remote source fixture

    Returns:
        Package: package test instance
    """
    packages = {"ahriman": package_description_ahriman}
    return Package(
        base="ahriman",
        version="2.6.0-1",
        remote=remote_source,
        packages=packages,
        packager="packager")


@pytest.fixture
def package_python_schedule(
        package_description_python_schedule: PackageDescription,
        package_description_python2_schedule: PackageDescription) -> Package:
    """
    multi package fixture

    Args:
        package_description_python_schedule(PackageDescription): description fixture
        package_description_python2_schedule(PackageDescription): description fixture

    Returns:
        Package: multi package test instance
    """
    packages = {
        "python-schedule": package_description_python_schedule,
        "python2-schedule": package_description_python2_schedule
    }
    return Package(
        base="python-schedule",
        version="1.0.0-2",
        remote=RemoteSource(
            source=PackageSource.AUR,
            git_url=AUR.remote_git_url("python-schedule", "aur"),
            web_url=AUR.remote_web_url("python-schedule"),
            path=".",
            branch="master",
        ),
        packages=packages)


@pytest.fixture
def package_description_ahriman() -> PackageDescription:
    """
    package description fixture

    Returns:
        PackageDescription: package description test instance
    """
    return PackageDescription(
        architecture="x86_64",
        archive_size=4200,
        build_date=42,
        depends=[
            "devtools",
            "git",
            "pyalpm",
            "python-cerberus",
            "python-inflection",
            "python-passlib",
            "python-requests",
            "python-setuptools",
            "python-srcinfo",
        ],
        make_depends=[
            "python-build",
            "python-installer",
            "python-wheel",
        ],
        opt_depends=[
            "breezy",
            "darcs",
            "mercurial",
            "python-aioauth-client",
            "python-aiohttp",
            "python-aiohttp-debugtoolbar",
            "python-aiohttp-jinja2",
            "python-aiohttp-security",
            "python-aiohttp-session",
            "python-boto3",
            "python-cryptography",
            "python-requests-unixsocket",
            "python-jinja",
            "rsync",
            "subversion",
        ],
        check_depends=[
            "python-pytest",
        ],
        description="ArcH linux ReposItory MANager",
        filename="ahriman-2.6.0-1-any.pkg.tar.zst",
        groups=[],
        installed_size=4200000,
        licenses=["GPL3"],
        url="https://github.com/arcan1s/ahriman",
    )


@pytest.fixture
def package_description_python_schedule() -> PackageDescription:
    """
    package description fixture

    Returns:
        PackageDescription: package description test instance
    """
    return PackageDescription(
        architecture="x86_64",
        archive_size=4201,
        build_date=421,
        depends=["python"],
        description="Python job scheduling for humans.",
        filename="python-schedule-1.0.0-2-any.pkg.tar.zst",
        groups=[],
        installed_size=4200001,
        licenses=["MIT"],
        url="https://github.com/dbader/schedule",
    )


@pytest.fixture
def package_description_python2_schedule() -> PackageDescription:
    """
    package description fixture

    Returns:
        PackageDescription: package description test instance
    """
    return PackageDescription(
        architecture="x86_64",
        archive_size=4202,
        build_date=422,
        depends=["python2"],
        description="Python job scheduling for humans.",
        filename="python2-schedule-1.0.0-2-any.pkg.tar.zst",
        groups=[],
        installed_size=4200002,
        licenses=["MIT"],
        url="https://github.com/dbader/schedule",
    )


@pytest.fixture
def parser() -> argparse.ArgumentParser:
    """
    fixture for command line arguments parser

    Returns:
        argparse.ArgumentParser: command line arguments parser instance
    """
    return _parser()


@pytest.fixture
def remote_source() -> RemoteSource:
    """
    remote source fixture

    Returns:
        RemoteSource: remote source test instance
    """
    return RemoteSource(
        source=PackageSource.AUR,
        git_url=AUR.remote_git_url("ahriman", "aur"),
        web_url=AUR.remote_web_url("ahriman"),
        path=".",
        branch="master",
    )


@pytest.fixture
def repository(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> Repository:
    """
    fixture for repository

    Args:
        configuration(Configuration): configuration fixture
        database(SQLite): database fixture
        mocker(MockerFixture): mocker object

    Returns:
        Repository: repository test instance
    """
    mocker.patch("ahriman.core.repository.Repository._set_context")
    _, repository_id = configuration.check_loaded()
    return Repository.load(repository_id, configuration, database, report=False)


@pytest.fixture
def repository_id() -> RepositoryId:
    """
    fixture for repository identifier

    Returns:
        RepositoryId: repository identifier test instance
    """
    return RepositoryId("x86_64", "aur")


@pytest.fixture
def repository_paths(configuration: Configuration) -> RepositoryPaths:
    """
    repository paths fixture

    Args:
        configuration(Configuration): configuration fixture

    Returns:
        RepositoryPaths: repository paths test instance
    """
    return configuration.repository_paths


@pytest.fixture
def result(package_ahriman: Package) -> Result:
    """
    result fixture

    Args:
        package_ahriman(Package): package fixture

    Returns:
        Result: result test instance
    """
    result = Result()
    result.add_updated(package_ahriman)
    return result


@pytest.fixture
def spawner(configuration: Configuration) -> Spawn:
    """
    spawner fixture

    Args:
        configuration(Configuration): configuration fixture

    Returns:
        Spawn: spawner fixture
    """
    return Spawn(MagicMock(), [
        "--configuration", str(configuration.path),
    ])


@pytest.fixture
def user() -> User:
    """
    fixture for user descriptor

    Returns:
        User: user descriptor instance
    """
    return User(username="user", password="pa55w0rd", access=UserAccess.Reporter, packager_id="packager", key="key")


@pytest.fixture
def watcher(local_client: Client) -> Watcher:
    """
    package status watcher fixture

    Args:
        local_client(Client): local status client fixture

    Returns:
        Watcher: package status watcher test instance
    """
    package_info = PackageInfo()
    event_bus = EventBus(0)
    return Watcher(local_client, package_info, event_bus)
