{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cell-23-20-00",
   "metadata": {},
   "source": [
    "# 23-20 · Тестируем перемещение и отмену\n",
    "\n",
    "Практика к разделу [«Проверяем перемещение и отмену»](../../site/chapters/glava-23/23-25-testy-peremeshheniya.html). Использует настоящий пакет `safesort`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "setup-23-20",
   "metadata": {},
   "source": [
    "## Reproducible local environment\n",
    "\n",
    "```bash\n",
    "git clone https://github.com/Cartesian-School/safesort.git\n",
    "cd safesort\n",
    "python3.14 -m venv .venv\n",
    "source .venv/bin/activate\n",
    "# Windows PowerShell: .venv\\Scripts\\Activate.ps1\n",
    "python -m pip install -U pip\n",
    "python -m pip install -e \".[dev]\"\n",
    "python -m pip install jupyter ipykernel\n",
    "python -m ipykernel install --user --name safesort-py314 --display-name \"SafeSort Python 3.14\"\n",
    "jupyter lab\n",
    "```\n",
    "\n",
    "Select the **SafeSort Python 3.14** kernel. The diagnostic cell below must\n",
    "point into this `.venv` and the cloned `src/safesort` tree."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "diagnostic-23-20",
   "metadata": {},
   "outputs": [],
   "source": [
    "import sys\n",
    "import safesort\n",
    "\n",
    "print(sys.executable)\n",
    "print(safesort.__file__)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-20-03",
   "metadata": {},
   "source": [
    "## Цель\n",
    "\n",
    "Написать и запустить три теста в духе тех, что живут в `projects/python/safesort/tests/test_executor.py` и `test_manifest.py`: успешное перемещение, полная отмена и конфликт при восстановлении."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-20-04",
   "metadata": {},
   "source": [
    "## Example — тесты apply_plan() и undo()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-23-20-05",
   "metadata": {},
   "outputs": [],
   "source": [
    "import tempfile\n",
    "from pathlib import Path\n",
    "\n",
    "from safesort.config import Config\n",
    "from safesort.scanner import scan\n",
    "from safesort.planner import build_plan\n",
    "from safesort.executor import apply_plan\n",
    "from safesort.manifest import write_manifest, undo\n",
    "from safesort.models import MoveOperation, SortPlan\n",
    "\n",
    "\n",
    "def test_apply_plan_moves_file_to_destination(tmp_path):\n",
    "    source = tmp_path / \"otchet.pdf\"\n",
    "    source.write_text(\"...\", encoding=\"utf-8\")\n",
    "    destination = tmp_path / \"Sorted\" / \"documents\" / \"otchet.pdf\"\n",
    "    plan = SortPlan(root=tmp_path, operations=(MoveOperation(source, destination),))\n",
    "\n",
    "    results = apply_plan(plan)\n",
    "\n",
    "    assert results[0].completed is True\n",
    "    assert not source.exists()\n",
    "    assert destination.exists()\n",
    "\n",
    "\n",
    "def test_undo_restores_original_location(tmp_path):\n",
    "    source = tmp_path / \"otchet.pdf\"\n",
    "    source.write_text(\"...\", encoding=\"utf-8\")\n",
    "    nastrojki = Config()\n",
    "    plan = build_plan(scan(tmp_path, nastrojki), tmp_path, nastrojki)\n",
    "    moves = apply_plan(plan)\n",
    "    manifest_obj, _ = write_manifest(tmp_path, moves)\n",
    "\n",
    "    result = undo(manifest_obj)\n",
    "\n",
    "    assert source.exists()\n",
    "    assert result.conflicts == ()\n",
    "\n",
    "\n",
    "def test_undo_refuses_to_overwrite_conflict(tmp_path):\n",
    "    source = tmp_path / \"otchet.pdf\"\n",
    "    source.write_text(\"оригинал\", encoding=\"utf-8\")\n",
    "    nastrojki = Config()\n",
    "    plan = build_plan(scan(tmp_path, nastrojki), tmp_path, nastrojki)\n",
    "    moves = apply_plan(plan)\n",
    "    manifest_obj, _ = write_manifest(tmp_path, moves)\n",
    "\n",
    "    source.write_text(\"кто-то создал новый файл здесь\", encoding=\"utf-8\")\n",
    "    result = undo(manifest_obj)\n",
    "\n",
    "    assert len(result.conflicts) == 1\n",
    "    assert source.read_text(encoding=\"utf-8\") == \"кто-то создал новый файл здесь\"\n",
    "\n",
    "\n",
    "for test_func in (\n",
    "    test_apply_plan_moves_file_to_destination,\n",
    "    test_undo_restores_original_location,\n",
    "    test_undo_refuses_to_overwrite_conflict,\n",
    "):\n",
    "    with tempfile.TemporaryDirectory() as tmp:\n",
    "        test_func(Path(tmp))\n",
    "    print(f\"OK: {test_func.__name__}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-20-06",
   "metadata": {},
   "source": [
    "## Starter\n",
    "\n",
    "Заполните отмеченное место. Неизменённый starter не проходит tests."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "task-23-20",
   "metadata": {
    "tags": [
     "exercise",
     "starter"
    ]
   },
   "outputs": [],
   "source": [
    "def test_apply_plan_reports_missing_source(tmp_path):\n",
    "    # TODO: build a plan for a path that was never created and assert failure.\n",
    "    raise NotImplementedError\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-20-08",
   "metadata": {},
   "source": [
    "## Task\n",
    "\n",
    "Допишите тест отсутствующего source: apply_plan должен вернуть completed=False и текст ошибки."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-20-09",
   "metadata": {},
   "source": [
    "## Tests\n",
    "\n",
    "Запустите после task cell: есть основной пример и хотя бы один крайний случай."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "tests-23-20",
   "metadata": {
    "tags": [
     "exercise-tests"
    ]
   },
   "outputs": [],
   "source": [
    "with tempfile.TemporaryDirectory() as tmp:\n",
    "    test_apply_plan_reports_missing_source(Path(tmp))\n",
    "\n",
    "# Edge case: an empty plan is a no-op.\n",
    "with tempfile.TemporaryDirectory() as tmp:\n",
    "    empty_root = Path(tmp)\n",
    "    assert apply_plan(SortPlan(root=empty_root, operations=())) == []\n",
    "    assert list(empty_root.iterdir()) == []\n",
    "print(\"Tests passed\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-20-11",
   "metadata": {},
   "source": [
    "## Hint\n",
    "\n",
    "Создайте только объекты Path и SortPlan; сам source на диске создавать не нужно."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-20-12",
   "metadata": {},
   "source": [
    "## Solution\n",
    "\n",
    "<details><summary>Показать решение после собственной попытки</summary>\n",
    "\n",
    "```python\n",
    "def test_apply_plan_reports_missing_source(tmp_path):\n",
    "    source = tmp_path / \"prizrak.pdf\"  # файла никогда не было\n",
    "    destination = tmp_path / \"Sorted\" / \"documents\" / \"prizrak.pdf\"\n",
    "    plan = SortPlan(root=tmp_path, operations=(MoveOperation(source, destination),))\n",
    "\n",
    "    results = apply_plan(plan)\n",
    "\n",
    "    assert results[0].completed is False\n",
    "    assert results[0].error is not None\n",
    "\n",
    "\n",
    "with tempfile.TemporaryDirectory() as tmp:\n",
    "    test_apply_plan_reports_missing_source(Path(tmp))\n",
    "print(\"OK: test_apply_plan_reports_missing_source\")\n",
    "```\n",
    "\n",
    "</details>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Cartesian Python 3.14",
   "language": "python",
   "name": "cartesian-python314"
  },
  "language_info": {
   "name": "python",
   "version": "3.14.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
