#!/usr/bin/python3
"""Tests for slpkg.install_data.InstallData."""

import json
from pathlib import Path
from unittest.mock import MagicMock

from slpkg.install_data import InstallData


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

SLACKBUILDS_TXT = """\
SLACKBUILD NAME: ffmpeg
SLACKBUILD LOCATION: ./multimedia/ffmpeg
SLACKBUILD FILES: ffmpeg.SlackBuild ffmpeg.info
SLACKBUILD VERSION: 6.1
SLACKBUILD DOWNLOAD: https://ffmpeg.org/ffmpeg-6.1.tar.xz
SLACKBUILD DOWNLOAD_x86_64: https://ffmpeg.org/ffmpeg-6.1-64.tar.xz
SLACKBUILD MD5SUM: abc123
SLACKBUILD MD5SUM_x86_64: def456
SLACKBUILD REQUIRES: libass x264
SLACKBUILD SHORT DESCRIPTION: ffmpeg A complete audio/video solution

"""

PACKAGES_TXT = """\
PACKAGE NAME:  ffmpeg-6.1-x86_64-1_SBo.txz
PACKAGE LOCATION:  ./l
PACKAGE SIZE (compressed):  12345 K
PACKAGE SIZE (uncompressed):  45678 K
PACKAGE REQUIRED:  libass,x264
PACKAGE DESCRIPTION:
ffmpeg ffmpeg: ffmpeg (A complete audio/video solution)

"""

CHECKSUMS_MD5 = """\
abc123  ./l/ffmpeg-6.1-x86_64-1_SBo.txz
"""

CHANGELOG_TXT = """\
Mon Jan  1 00:00:00 UTC 2026
patches/packages/ffmpeg-6.1-x86_64-1_SBo.txz: Upgraded.
"""


def make_install_data(tmp_path: Path) -> InstallData:
    """Construct InstallData bypassing __init__ (avoids config_load/Repositories)."""
    obj: InstallData = object.__new__(InstallData)
    obj.cpu_arch = "x86_64"
    obj.package_type = [".tgz", ".txz", ".tbz", ".tlz"]

    obj.utils = MagicMock()
    obj.utils.split_package.side_effect = lambda pkg: {
        "name": pkg.rsplit("-", 3)[0],
        "version": pkg.rsplit("-", 3)[1] if len(pkg.rsplit("-", 3)) > 1 else "",
        "arch": pkg.rsplit("-", 3)[2] if len(pkg.rsplit("-", 3)) > 2 else "",
        "build": pkg.rsplit("-", 3)[3].split(".")[0] if len(pkg.rsplit("-", 3)) > 3 else "",
    }

    repos = MagicMock()
    repos.data_json = "data.json"
    repos.repos_information = tmp_path / "repos_info.json"
    repos.sbo_repo_name = "sbo"
    repos.ponce_repo_name = "ponce"
    repos.slack_patches_repo_name = "slack_patches"
    repos.slack_extra_repo_name = "slack_extra"
    repos.slack_testing_repo_name = "slack_testing"
    repos.repositories = {}
    obj.repos = repos

    obj.view_process = MagicMock()
    return obj


def make_sbo_repo(tmp_path: Path, obj: InstallData) -> Path:
    """Set up a minimal SBo repo structure and configure obj.repos for it."""
    repo_path = tmp_path / "sbo"
    repo_path.mkdir()
    (repo_path / "SLACKBUILDS.TXT").write_text(SLACKBUILDS_TXT, encoding="utf-8")
    (repo_path / "ChangeLog.txt").write_text(CHANGELOG_TXT, encoding="utf-8")

    obj.repos.repositories["sbo"] = {
        "path": repo_path,
        "slackbuilds_txt": "SLACKBUILDS.TXT",
        "changelog_txt": "ChangeLog.txt",
        "repo_tag": "_SBo",
    }
    return repo_path


def make_binary_repo(tmp_path: Path, obj: InstallData) -> Path:
    """Set up a minimal binary repo structure and configure obj.repos for it."""
    repo_path = tmp_path / "slack"
    repo_path.mkdir()
    (repo_path / "PACKAGES.TXT").write_text(PACKAGES_TXT, encoding="utf-8")
    (repo_path / "CHECKSUMS.md5").write_text(CHECKSUMS_MD5, encoding="utf-8")
    (repo_path / "ChangeLog.txt").write_text(CHANGELOG_TXT, encoding="utf-8")

    obj.utils.read_text_file.side_effect = lambda p: Path(p).read_text(encoding="utf-8").splitlines(keepends=True)

    obj.repos.repositories["slack"] = {
        "path": repo_path,
        "packages_txt": "PACKAGES.TXT",
        "checksums_md5": "CHECKSUMS.md5",
        "changelog_txt": "ChangeLog.txt",
        "mirror_packages": "https://mirrors.slackware.com/slackware/slackware64-15.0/",
        "mirror_changelog": "https://mirrors.slackware.com/slackware/",
    }
    return repo_path


# ---------------------------------------------------------------------------
# write_repo_info
# ---------------------------------------------------------------------------


def test_write_repo_info_extracts_date_from_changelog(tmp_path: Path) -> None:
    obj = make_install_data(tmp_path)
    changelog = tmp_path / "ChangeLog.txt"
    changelog.write_text(CHANGELOG_TXT, encoding="utf-8")
    obj.utils.read_text_file.return_value = CHANGELOG_TXT.splitlines(keepends=True)

    obj.write_repo_info(changelog, {"repo_name": "sbo", "full_requires": False})

    written = json.loads(obj.repos.repos_information.read_text(encoding="utf-8"))
    assert "Mon Jan" in written["sbo"]["last_updated"]


def test_write_repo_info_missing_changelog_writes_empty_date(tmp_path: Path) -> None:
    obj = make_install_data(tmp_path)
    missing = tmp_path / "nonexistent.txt"

    obj.write_repo_info(missing, {"repo_name": "sbo", "full_requires": False})

    written = json.loads(obj.repos.repos_information.read_text(encoding="utf-8"))
    assert written["sbo"]["last_updated"] == ""


def test_write_repo_info_stores_full_requires_flag(tmp_path: Path) -> None:
    obj = make_install_data(tmp_path)
    changelog = tmp_path / "ChangeLog.txt"
    changelog.write_text("", encoding="utf-8")
    obj.utils.read_text_file.return_value = []

    obj.write_repo_info(changelog, {"repo_name": "myrepo", "full_requires": True})

    written = json.loads(obj.repos.repos_information.read_text(encoding="utf-8"))
    assert written["myrepo"]["full_requires"] is True


def test_write_repo_info_merges_with_existing_data(tmp_path: Path) -> None:
    obj = make_install_data(tmp_path)
    # Pre-populate repos_info.json with existing data
    existing = {"other_repo": {"last_updated": "2025-01-01", "full_requires": False}}
    obj.repos.repos_information.write_text(json.dumps(existing), encoding="utf-8")
    obj.utils.read_json_file.return_value = existing

    changelog = tmp_path / "ChangeLog.txt"
    changelog.write_text("", encoding="utf-8")
    obj.utils.read_text_file.return_value = []

    obj.write_repo_info(changelog, {"repo_name": "sbo", "full_requires": False})

    written = json.loads(obj.repos.repos_information.read_text(encoding="utf-8"))
    assert "other_repo" in written
    assert "sbo" in written


# ---------------------------------------------------------------------------
# install_sbo_data — parsing
# ---------------------------------------------------------------------------


def test_install_sbo_data_creates_data_json(tmp_path: Path) -> None:
    obj = make_install_data(tmp_path)
    repo_path = make_sbo_repo(tmp_path, obj)
    obj.utils.read_text_file.return_value = CHANGELOG_TXT.splitlines(keepends=True)

    obj.install_sbo_data("sbo")

    data_file = repo_path / "data.json"
    assert data_file.exists()
    data = json.loads(data_file.read_text(encoding="utf-8"))
    assert "ffmpeg" in data


def test_install_sbo_data_parses_version(tmp_path: Path) -> None:
    obj = make_install_data(tmp_path)
    repo_path = make_sbo_repo(tmp_path, obj)
    obj.utils.read_text_file.return_value = []

    obj.install_sbo_data("sbo")

    data = json.loads((repo_path / "data.json").read_text(encoding="utf-8"))
    assert data["ffmpeg"]["version"] == "6.1"


def test_install_sbo_data_parses_location(tmp_path: Path) -> None:
    obj = make_install_data(tmp_path)
    repo_path = make_sbo_repo(tmp_path, obj)
    obj.utils.read_text_file.return_value = []

    obj.install_sbo_data("sbo")

    data = json.loads((repo_path / "data.json").read_text(encoding="utf-8"))
    assert data["ffmpeg"]["location"] == "multimedia"


def test_install_sbo_data_parses_requires(tmp_path: Path) -> None:
    obj = make_install_data(tmp_path)
    repo_path = make_sbo_repo(tmp_path, obj)
    obj.utils.read_text_file.return_value = []

    obj.install_sbo_data("sbo")

    data = json.loads((repo_path / "data.json").read_text(encoding="utf-8"))
    assert "libass" in data["ffmpeg"]["requires"]
    assert "x264" in data["ffmpeg"]["requires"]


def test_install_sbo_data_filters_readme_from_requires(tmp_path: Path) -> None:
    txt = SLACKBUILDS_TXT.replace(
        "SLACKBUILD REQUIRES: libass x264",
        "SLACKBUILD REQUIRES: %README% libass"
    )
    obj = make_install_data(tmp_path)
    repo_path = tmp_path / "sbo"
    repo_path.mkdir(exist_ok=True)
    (repo_path / "SLACKBUILDS.TXT").write_text(txt, encoding="utf-8")
    (repo_path / "ChangeLog.txt").write_text("", encoding="utf-8")
    obj.repos.repositories["sbo"] = {
        "path": repo_path,
        "slackbuilds_txt": "SLACKBUILDS.TXT",
        "changelog_txt": "ChangeLog.txt",
        "repo_tag": "_SBo",
    }
    obj.utils.read_text_file.return_value = []

    obj.install_sbo_data("sbo")

    data = json.loads((repo_path / "data.json").read_text(encoding="utf-8"))
    assert "%README%" not in data["ffmpeg"]["requires"]
    assert "libass" in data["ffmpeg"]["requires"]


def test_install_sbo_data_noarch_from_slackbuild(tmp_path: Path) -> None:
    obj = make_install_data(tmp_path)
    repo_path = make_sbo_repo(tmp_path, obj)
    # Create a SlackBuild script that declares ARCH=noarch
    sbo_dir = repo_path / "multimedia" / "ffmpeg"
    sbo_dir.mkdir(parents=True)
    (sbo_dir / "ffmpeg.SlackBuild").write_text(
        "#!/bin/bash\nARCH=noarch\nBUILD=${BUILD:-1}\n", encoding="utf-8"
    )
    obj.utils.read_text_file.return_value = []

    obj.install_sbo_data("sbo")

    data = json.loads((repo_path / "data.json").read_text(encoding="utf-8"))
    assert data["ffmpeg"]["arch"] == "noarch"


def test_install_sbo_data_missing_slackbuilds_txt_returns_early(tmp_path: Path) -> None:
    obj = make_install_data(tmp_path)
    repo_path = tmp_path / "sbo"
    repo_path.mkdir()
    obj.repos.repositories["sbo"] = {
        "path": repo_path,
        "slackbuilds_txt": "SLACKBUILDS.TXT",
        "changelog_txt": "ChangeLog.txt",
        "repo_tag": "_SBo",
    }

    obj.install_sbo_data("sbo")  # must not raise

    assert not (repo_path / "data.json").exists()


# ---------------------------------------------------------------------------
# install_binary_data — parsing
# ---------------------------------------------------------------------------


def test_install_binary_data_creates_data_json(tmp_path: Path) -> None:
    obj = make_install_data(tmp_path)
    repo_path = make_binary_repo(tmp_path, obj)

    obj.install_binary_data("slack")

    data_file = repo_path / "data.json"
    assert data_file.exists()
    data = json.loads(data_file.read_text(encoding="utf-8"))
    assert "ffmpeg" in data


def test_install_binary_data_parses_version(tmp_path: Path) -> None:
    obj = make_install_data(tmp_path)
    repo_path = make_binary_repo(tmp_path, obj)

    obj.install_binary_data("slack")

    data = json.loads((repo_path / "data.json").read_text(encoding="utf-8"))
    assert data["ffmpeg"]["version"] == "6.1"


def test_install_binary_data_parses_location(tmp_path: Path) -> None:
    obj = make_install_data(tmp_path)
    repo_path = make_binary_repo(tmp_path, obj)

    obj.install_binary_data("slack")

    data = json.loads((repo_path / "data.json").read_text(encoding="utf-8"))
    assert data["ffmpeg"]["location"] == "l"


def test_install_binary_data_parses_requires(tmp_path: Path) -> None:
    obj = make_install_data(tmp_path)
    repo_path = make_binary_repo(tmp_path, obj)

    obj.install_binary_data("slack")

    data = json.loads((repo_path / "data.json").read_text(encoding="utf-8"))
    assert "libass" in data["ffmpeg"]["requires"]
    assert "x264" in data["ffmpeg"]["requires"]


def test_install_binary_data_missing_checksum_marked_error(tmp_path: Path) -> None:
    obj = make_install_data(tmp_path)
    repo_path = make_binary_repo(tmp_path, obj)
    # Empty checksums file — no match for ffmpeg package
    (repo_path / "CHECKSUMS.md5").write_text("", encoding="utf-8")

    obj.install_binary_data("slack")

    data = json.loads((repo_path / "data.json").read_text(encoding="utf-8"))
    assert data["ffmpeg"]["checksum"] == "error checksum"


def test_install_binary_data_full_requires_pipe_syntax(tmp_path: Path) -> None:
    txt = PACKAGES_TXT.replace(
        "PACKAGE REQUIRED:  libass,x264",
        "PACKAGE REQUIRED:  libx264|x264,libass"
    )
    obj = make_install_data(tmp_path)
    repo_path = tmp_path / "slack"
    repo_path.mkdir(exist_ok=True)
    (repo_path / "PACKAGES.TXT").write_text(txt, encoding="utf-8")
    (repo_path / "CHECKSUMS.md5").write_text("", encoding="utf-8")
    (repo_path / "ChangeLog.txt").write_text("", encoding="utf-8")
    obj.utils.read_text_file.side_effect = lambda p: Path(p).read_text(encoding="utf-8").splitlines(keepends=True)
    obj.repos.repositories["slack"] = {
        "path": repo_path,
        "packages_txt": "PACKAGES.TXT",
        "checksums_md5": "CHECKSUMS.md5",
        "changelog_txt": "ChangeLog.txt",
        "mirror_packages": "https://mirrors.slackware.com/",
        "mirror_changelog": "https://mirrors.slackware.com/",
    }

    obj.install_binary_data("slack")

    data = json.loads((repo_path / "data.json").read_text(encoding="utf-8"))
    # With '|' syntax, x264 (second option) should be chosen
    assert "x264" in data["ffmpeg"]["requires"]


def test_install_binary_data_missing_packages_txt_returns_early(tmp_path: Path) -> None:
    obj = make_install_data(tmp_path)
    repo_path = tmp_path / "slack"
    repo_path.mkdir()
    obj.utils.read_text_file.side_effect = FileNotFoundError
    obj.repos.repositories["slack"] = {
        "path": repo_path,
        "packages_txt": "PACKAGES.TXT",
        "checksums_md5": "CHECKSUMS.md5",
        "changelog_txt": "ChangeLog.txt",
        "mirror_packages": "https://mirrors.slackware.com/",
        "mirror_changelog": "https://mirrors.slackware.com/",
    }

    obj.install_binary_data("slack")  # must not raise

    assert not (repo_path / "data.json").exists()
