from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
import sys

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))

from models import Identity, InventoryItem
from planner import Planner


class FakeRules:
    """Small deterministic crafting graph used to verify planner depth logic."""

    def __init__(self, recipes: dict[tuple[str, str], str]):
        self.recipes = {tuple(sorted(pair)): result for pair, result in recipes.items()}
        self.reverse: dict[str, list[tuple[str, str]]] = {}
        for (left, right), result in self.recipes.items():
            self.reverse.setdefault(result, []).append((left, right))

    @staticmethod
    def _identity(name: str) -> Identity:
        return Identity("test", name, "", "", False)

    def combine_identity(self, left: Identity, right: Identity):
        result = self.recipes.get(tuple(sorted((left.base, right.base))))
        return (self._identity(result), []) if result else (None, [])

    def reverse_recipes(self, target: Identity, reachable=None, limit: int = 500):
        pairs = self.reverse.get(target.base, [])[:limit]
        return [(self._identity(left), self._identity(right)) for left, right in pairs]

    @staticmethod
    def identity_display(identity: Identity) -> str:
        return identity.base


def _item(name: str, index: int) -> InventoryItem:
    identity = Identity("test", name, "", "", False)
    return InventoryItem(
        db_id=index,
        profile_id=None,
        import_id=None,
        item_id=str(index),
        full_name=name,
        identity=identity,
        quality="Zwykły",
        upgrade=1,
        level=1,
    )


def test_inventory_plan_can_use_two_first_degree_results_for_second_degree_target():
    # A+C -> X, B+D -> Z, then X+Z -> E.
    rules = FakeRules({("A", "C"): "X", ("B", "D"): "Z", ("X", "Z"): "E"})
    inventory = [_item(name, index) for index, name in enumerate(("A", "B", "C", "D"), 1)]

    plans = Planner(rules).find_plans(
        Identity("test", "E", "", "", False),
        inventory,
        max_depth=2,
        top_k=10,
        timeout_seconds=5,
        allow_missing=False,
        stop_at_first_success=False,
    )

    assert plans
    assert any(
        plan.craft_depth == 2
        and plan.step_count == 3
        and plan.missing_count == 0
        and {step.result.base for step in plan.steps} >= {"X", "Z", "E"}
        for plan in plans
    )


def test_full_depth_mode_keeps_deeper_plan_even_after_many_shallow_plans():
    recipes: dict[tuple[str, str], str] = {
        ("A", "C"): "X",
        ("B", "D"): "Z",
        ("X", "Z"): "E",
    }
    inventory_names = ["A", "B", "C", "D"]
    # More shallow E plans than the requested top_k.  This reproduced the old
    # behaviour where the planner stopped at depth 1 and never exposed depth 2.
    for index in range(20):
        left = f"L{index}"
        right = f"R{index}"
        inventory_names.extend((left, right))
        recipes[(left, right)] = "E"

    rules = FakeRules(recipes)
    inventory = [_item(name, index) for index, name in enumerate(inventory_names, 1)]
    target = Identity("test", "E", "", "", False)

    quick = Planner(rules).find_plans(
        target,
        inventory,
        max_depth=2,
        top_k=6,
        timeout_seconds=5,
        allow_missing=False,
        stop_at_first_success=True,
    )
    assert quick
    assert {plan.craft_depth for plan in quick} == {1}

    full = Planner(rules).find_plans(
        target,
        inventory,
        max_depth=2,
        top_k=6,
        timeout_seconds=5,
        allow_missing=False,
        stop_at_first_success=False,
    )
    assert full
    assert 1 in {plan.craft_depth for plan in full}
    assert 2 in {plan.craft_depth for plan in full}


def test_combo_arrow_asset_is_bundled():
    assert (ROOT / "assets" / "awakening" / "combo_down_arrow.png").is_file()
