"""Tests for distribution_name resolution and menuinst.toml tracking."""

from __future__ import annotations

import json
import logging
import os
import subprocess
import sys
from contextlib import contextmanager
from typing import TYPE_CHECKING

import pytest

from menuinst.api import (
    _install_adapter,
    record_shortcuts,
    remove_shortcut_records,
    write_menuinst_toml,
)
from menuinst.platforms import Menu
from menuinst.utils import MENUINST_TOML_SCHEMA_VERSION, parse_schemaver, read_menuinst_toml

if TYPE_CHECKING:
    from pathlib import Path

# Placeholder distribution names for tests
DIST_NAME = "Something"
DIST_NAME_ALT = "SomethingElse"


@contextmanager
def make_readonly(path: "Path"):
    """Make a path read-only, restoring permissions on exit."""
    if sys.platform == "win32":
        subprocess.run(["icacls", str(path), "/deny", f"{os.getlogin()}:(W)"], check=True)
        try:
            yield
        finally:
            subprocess.run(["icacls", str(path), "/remove:d", os.getlogin()], check=True)
    else:
        os.chmod(path, 0o555)
        try:
            yield
        finally:
            os.chmod(path, 0o755)


class TestGetDistributionName:
    """Tests for Menu._get_distribution_name() resolution order."""

    def test_env_var_takes_priority(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """MENUINST_DISTRIBUTION_NAME env var should be used when set."""
        monkeypatch.setenv("MENUINST_DISTRIBUTION_NAME", DIST_NAME)
        menu = Menu("test", prefix=str(tmp_path), base_prefix=str(tmp_path))
        assert menu._get_distribution_name() == DIST_NAME
        assert menu.placeholders["DISTRIBUTION_NAME"] == DIST_NAME

    def test_toml_used_when_no_env_var(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """TOML value should be used when env var is not set."""
        monkeypatch.delenv("MENUINST_DISTRIBUTION_NAME", raising=False)
        write_menuinst_toml(tmp_path, {"distribution_name": DIST_NAME})
        menu = Menu("test", prefix=str(tmp_path), base_prefix=str(tmp_path))
        assert menu._get_distribution_name() == DIST_NAME

    def test_fallback_to_base_prefix_name(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Should fall back to base_prefix.name when no env var or TOML."""
        monkeypatch.delenv("MENUINST_DISTRIBUTION_NAME", raising=False)
        menu = Menu("test", prefix=str(tmp_path), base_prefix=str(tmp_path))
        assert menu._get_distribution_name() == tmp_path.name

    def test_env_var_overrides_toml(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Env var should take priority over TOML value."""
        monkeypatch.setenv("MENUINST_DISTRIBUTION_NAME", DIST_NAME)
        write_menuinst_toml(tmp_path, {"distribution_name": DIST_NAME_ALT})
        menu = Menu("test", prefix=str(tmp_path), base_prefix=str(tmp_path))
        assert menu._get_distribution_name() == DIST_NAME

    def test_malformed_toml_raises(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Malformed TOML should raise an exception."""
        monkeypatch.delenv("MENUINST_DISTRIBUTION_NAME", raising=False)
        toml_path = tmp_path / "Menu" / "menuinst.toml"
        toml_path.parent.mkdir(parents=True, exist_ok=True)
        toml_path.write_text("this is not valid toml {{{{")
        with pytest.raises(ValueError, match="Failed to read"):
            # On Linux, Menu() triggers _get_distribution_name() during __init__,
            # but on Windows/macOS it's lazy. Call it explicitly to ensure the
            # error is raised on all platforms.
            menu = Menu("test", prefix=str(tmp_path), base_prefix=str(tmp_path))
            menu._get_distribution_name()


class TestShortcutRecording:
    """Tests for shortcut recording and removal in menuinst.toml."""

    def test_install_records_to_toml(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """install() should record shortcuts to menuinst.toml."""
        monkeypatch.delenv("MENUINST_DISTRIBUTION_NAME", raising=False)
        base_prefix = tmp_path / "base"
        base_prefix.mkdir()

        # Test via record_shortcuts directly
        record_shortcuts(
            base_prefix,
            base_prefix,
            "foo.json",
            [tmp_path / "foo.lnk", tmp_path / "bar.lnk"],
            distribution_name=DIST_NAME,
        )

        data = read_menuinst_toml(base_prefix)
        assert data["distribution_name"] == DIST_NAME
        assert len(data["shortcuts"]) == 2
        assert data["shortcuts"][0]["source"] == "foo.json"

    def test_remove_cleans_toml(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """remove() should clean up TOML entries."""
        monkeypatch.delenv("MENUINST_DISTRIBUTION_NAME", raising=False)
        base_prefix = tmp_path / "base"
        base_prefix.mkdir()

        # Pre-populate TOML with shortcuts from two sources
        write_menuinst_toml(
            base_prefix,
            {
                "distribution_name": DIST_NAME,
                "shortcuts": [
                    {"source": "foo.json", "path": "/path/to/foo.lnk"},
                    {"source": "foo.json", "path": "/path/to/bar.lnk"},
                    {"source": "baz.json", "path": "/path/to/baz.lnk"},
                ],
            },
        )

        # Remove records for foo.json
        remove_shortcut_records(base_prefix, "foo.json")

        data = read_menuinst_toml(base_prefix)
        assert len(data["shortcuts"]) == 1
        assert data["shortcuts"][0]["source"] == "baz.json"
        # distribution_name should be preserved
        assert data["distribution_name"] == DIST_NAME

    def test_distribution_name_only_written_to_base_prefix(self, tmp_path: Path) -> None:
        """distribution_name should only be written when prefix == base_prefix."""
        base_prefix = tmp_path / "base"
        env_prefix = tmp_path / "envs" / "foo"
        base_prefix.mkdir(parents=True)
        env_prefix.mkdir(parents=True)

        # Record to base prefix - should include distribution_name
        record_shortcuts(
            base_prefix,
            base_prefix,
            "foo.json",
            [tmp_path / "foo.lnk"],
            distribution_name=DIST_NAME,
        )
        data = read_menuinst_toml(base_prefix)
        assert data.get("distribution_name") == DIST_NAME

        # Record to non-base prefix - should NOT include distribution_name
        record_shortcuts(
            env_prefix,
            base_prefix,
            "bar.json",
            [tmp_path / "bar.lnk"],
            distribution_name=DIST_NAME,
        )
        data = read_menuinst_toml(env_prefix)
        assert "distribution_name" not in data
        assert len(data["shortcuts"]) == 1

    def test_record_shortcuts_handles_permission_error(self, tmp_path: Path, caplog) -> None:
        """record_shortcuts() should not raise when prefix is read-only."""
        prefix = tmp_path / "readonly"
        prefix.mkdir()
        menu_dir = prefix / "Menu"
        menu_dir.mkdir()

        # Pre-create empty TOML so the test is consistent with test_remove_* below
        write_menuinst_toml(prefix, {})

        with make_readonly(menu_dir), make_readonly(prefix), caplog.at_level(logging.DEBUG):
            # This should NOT raise PermissionError
            record_shortcuts(
                prefix=prefix,
                base_prefix=prefix,
                source="test.json",
                paths=[tmp_path / "fake" / "path" / "shortcut.desktop"],
            )

        assert "permission denied" in caplog.text.lower()

    def test_remove_shortcut_records_handles_permission_error(
        self, tmp_path: Path, caplog
    ) -> None:
        """remove_shortcut_records() should not raise when prefix is read-only."""
        prefix = tmp_path / "prefix"
        prefix.mkdir()
        menu_dir = prefix / "Menu"
        menu_dir.mkdir()

        write_menuinst_toml(
            prefix,
            {"shortcuts": [{"source": "test.json", "path": "/fake/path"}]},
        )

        with make_readonly(menu_dir), make_readonly(prefix), caplog.at_level(logging.DEBUG):
            # This should NOT raise PermissionError
            remove_shortcut_records(prefix, "test.json")

        assert "permission denied" in caplog.text.lower()


class TestInstallAdapter:
    """Tests for _install_adapter recording correct source filename."""

    def test_records_actual_filename_not_menu_name(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """_install_adapter should record JSON filename, not rendered menu_name."""
        monkeypatch.delenv("MENUINST_DISTRIBUTION_NAME", raising=False)
        (tmp_path / ".nonadmin").touch()
        menu_dir = tmp_path / "Menu"
        menu_dir.mkdir()

        # Create JSON with menu_name containing placeholder
        json_file = menu_dir / "test_shortcut.json"
        json_file.write_text(
            json.dumps(
                {
                    "$schema": "https://json-schema.org/draft-07/schema",
                    "menu_name": "{{ DISTRIBUTION_NAME }} Foo Bar",
                    "menu_items": [
                        {
                            "name": "Foo Bar",
                            "command": ["echo", "test"],
                            "activate": False,
                            "platforms": {"linux": {}, "win": {}, "osx": {}},
                        }
                    ],
                }
            )
        )

        _install_adapter(str(json_file), prefix=str(tmp_path), root_prefix=str(tmp_path))

        data = read_menuinst_toml(tmp_path)
        # Source should be the filename, not "{{ DISTRIBUTION_NAME }} Foo Bar.json"
        assert data["shortcuts"][0]["source"] == "test_shortcut.json"


class TestSchemaVersion:
    """Tests for SchemaVer parsing and TOML schema version handling."""

    @pytest.mark.parametrize(
        "version,expected",
        [
            ("1-0-0", (1, 0, 0)),
            ("1-1-3", (1, 1, 3)),
            ("2-0-0", (2, 0, 0)),
            ("10-20-30", (10, 20, 30)),
        ],
    )
    def test_parse_schemaver_valid(self, version: str, expected: tuple[int, int, int]) -> None:
        """Valid SchemaVer strings should parse correctly."""
        assert parse_schemaver(version) == expected

    @pytest.mark.parametrize(
        "version",
        [
            "1",
            "1-0",
            "1.0.0",
            "1-0-0-0",
            "a-b-c",
            "",
        ],
    )
    def test_parse_schemaver_invalid(self, version: str) -> None:
        """Invalid SchemaVer strings should raise ValueError."""
        with pytest.raises(ValueError):
            parse_schemaver(version)

    def test_toml_writes_schemaver_format(self, tmp_path: Path) -> None:
        """write_menuinst_toml should write schema_version in SchemaVer format."""
        write_menuinst_toml(tmp_path, {"distribution_name": "Test"})
        data = read_menuinst_toml(tmp_path)
        assert data["schema_version"] == MENUINST_TOML_SCHEMA_VERSION
        # Verify it's a valid SchemaVer string
        parse_schemaver(data["schema_version"])
