import sqlite3
import sys
from pathlib import Path

import pytest

from cook import Manager, Task
from cook.actions import CompositeAction, FunctionAction, ModuleAction, SubprocessAction
from cook.contexts import (
    Context,
    FunctionContext,
    create_group,
    create_target_directories,
    normalize_action,
    normalize_dependencies,
)
from cook.controller import Controller


def test_function_context(m: Manager) -> None:
    tasks: list[Task] = []

    def func(t: Task) -> Task:
        tasks.append(t)
        return t

    with FunctionContext(func):
        m.create_task("my-task")
    m.create_task("my-other-task")

    (task,) = tasks
    assert task.name == "my-task"


def test_missing_task_context(m: Manager) -> None:
    with (
        pytest.raises(ValueError, match="did not return a task"),
        FunctionContext(lambda _: None),  # type: ignore[arg-type]
    ):
        m.create_task("my-task")


def test_context_management(m: Manager) -> None:
    with pytest.raises(RuntimeError, match="no active contexts"), Context():
        m.contexts = []
    with pytest.raises(RuntimeError, match="unexpected context"), Context():
        m.contexts.append("something else")  # type: ignore[arg-type]


@pytest.mark.asyncio
async def test_create_target_directories(
    m: Manager, tmp_wd: Path, conn: sqlite3.Connection
) -> None:
    filename = tmp_wd / "this/is/a/hierarchy.txt"
    with normalize_action(), create_target_directories():
        task = m.create_task("foo", targets=[filename], action=["touch", filename])
    assert not filename.parent.is_dir()

    controller = Controller(m.resolve_dependencies(), conn)
    await controller.execute(task)
    assert filename.parent.is_dir()


@pytest.mark.asyncio
async def test_create_target_directories_with_multiple_targets(
    m: Manager, tmp_wd: Path, conn: sqlite3.Connection
) -> None:
    filenames = [
        tmp_wd / "this/is/a/hierarchy.txt",
        tmp_wd / "this/is/a/hierarchy2.txt",
    ]
    filename: Path | None = None
    task: Task | None = None
    with normalize_action(), create_target_directories():
        for filename in filenames:
            task = m.create_task(
                filename.name, targets=[filename], action=["touch", filename]
            )
    assert filename is not None and task is not None
    assert not filename.parent.is_dir()

    controller = Controller(m.resolve_dependencies(), conn)
    await controller.execute(task)
    assert filename.parent.is_dir()


@pytest.mark.asyncio
async def test_create_target_directories_different_parents(
    m: Manager, tmp_wd: Path, conn: sqlite3.Connection
) -> None:
    # Targets in two different directories: the closure in create_target_directories
    # must create both parents, not just the last one from the loop.
    targets = [tmp_wd / "dir_a" / "file.txt", tmp_wd / "dir_b" / "file.txt"]
    with normalize_action(), create_target_directories():
        task = m.create_task(
            "multi_dir",
            targets=targets,
            action=FunctionAction(
                lambda t: [target.write_text("ok") for target in t.targets]
            ),
        )
    controller = Controller(m.resolve_dependencies(), conn)
    await controller.execute(task)
    for target in targets:
        assert target.is_file(), f"{target} was not created"


def test_normalize_action(m: Manager) -> None:
    with normalize_action():
        task = m.create_task("foo", action="bar")
        assert (
            isinstance(task.action, SubprocessAction)
            and task.action.args == "bar"
            and task.action.kwargs["shell"]
        )

        task = m.create_task("bar", action=["baz"])
        assert (
            isinstance(task.action, SubprocessAction)
            and task.action.args == ["baz"]
            and not task.action.kwargs.get("shell")
        )

        actions = [SubprocessAction("hello", shell=True), SubprocessAction("world")]
        task = m.create_task("baz", action=actions)
        assert isinstance(
            task.action, CompositeAction
        ) and task.action.actions == tuple(actions)

        task = m.create_task("xyz", action=lambda x: None)
        assert isinstance(task.action, FunctionAction)

        task = m.create_task("fizz", action=[pytest, "foo", "bar"])
        assert isinstance(task.action, ModuleAction) and task.action.args == [
            sys.executable,
            "-m",
            "pytest",
            "foo",
            "bar",
        ]

        with pytest.raises(ValueError, match="must not be an empty list"):
            m.create_task("buzz", action=[])


def test_group_no_tasks(m: Manager) -> None:
    with pytest.raises(RuntimeError, match="no tasks"), create_group("g"):
        pass


def test_group(m: Manager) -> None:
    with create_group("g"):
        t1 = m.create_task("t1")
        t2 = m.create_task("t2")
    assert m.tasks["g"].task_dependencies == [t1, t2]


def test_normalize_dependencies(m: Manager) -> None:
    with create_group("g") as g:
        _base = m.create_task("base")
    with normalize_dependencies():
        task = m.create_task("task3", task_dependencies=["g"])
        assert task.task_dependencies == [g.task]


def test_normalize_dependencies_deprecated_syntax(m: Manager) -> None:
    """Test that passing Tasks to dependencies parameter emits DeprecationWarning."""
    with create_group("g") as g:
        base = m.create_task("base")
    with normalize_dependencies():
        # Test with group
        with pytest.warns(
            DeprecationWarning,
            match="Passing Task objects to 'dependencies' is deprecated",
        ):
            task = m.create_task("task1", dependencies=[g])
        assert task.task_dependencies == [g.task]

        # Test with regular task
        with pytest.warns(
            DeprecationWarning,
            match="Passing Task objects to 'dependencies' is deprecated",
        ):
            task = m.create_task("task2", dependencies=[base])
        assert task.task_dependencies == [base]
