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

from crafting import combine_items
from database import Database
from inventory_parser import parse_armory_html
from models import Identity, InventoryItem, quality_level
from planner import Planner
from rules import RuleBook

RULES = RuleBook(ROOT / "data")


def item(identity: Identity, quality: str, upgrade: int, name: str = "test") -> InventoryItem:
    return InventoryItem(None, None, None, "", name, identity, quality, upgrade, quality_level(quality, upgrade))


def test_identical_item_progression_crosses_quality_boundary():
    ident = Identity("melee1h", "piesc niebios", "lekki", "trafienia", False)
    a = item(ident, "Dobry", 3)
    b = item(ident, "Zwykły", 5)
    result = combine_items(a, b, RULES)
    assert result is not None
    assert result.result_level == quality_level("Dobry", 0)


def test_identical_item_gets_plus_one():
    ident = Identity("melee1h", "piesc niebios", "lekki", "zwinnosci", False)
    result = combine_items(item(ident, "Dobry", 0), item(ident, "Dobry", 0), RULES)
    assert result is not None
    assert result.result_level == quality_level("Dobry", 1)


def test_blank_affix_removes_affix():
    left = Identity("head", "helm", "krwawy", "kary", False)
    right = Identity("head", "kask", "", "", False)
    result, notes = RULES.combine_identity(left, right)
    assert not notes
    assert result is not None
    assert result.prefix == ""
    assert result.suffix == ""


def test_spodnie_szorty_override():
    left = Identity("pants", "spodnie", "", "", False)
    right = Identity("pants", "szorty", "", "", False)
    result, notes = RULES.combine_identity(left, right)
    assert result is not None
    assert result.base == "spodnica"


def test_verified_asymmetric_base_overrides():
    cases = [
        ("head", "kapelusz", "bandana", "bandana"),
        ("pants", "szorty", "kilt", "kilt"),
        ("melee1h", "topor", "piesc niebios", "wakizashi"),
    ]
    for category, left_base, right_base, expected in cases:
        left = Identity(category, left_base, "", "", False)
        right = Identity(category, right_base, "", "", False)
        result, notes = RULES.combine_identity(left, right)
        assert not notes
        assert result is not None
        assert result.base == expected
        # Ingredient order must not matter.
        reverse, reverse_notes = RULES.combine_identity(right, left)
        assert not reverse_notes
        assert reverse is not None
        assert reverse.base == expected


def test_verified_opiekunczy_demoniczny_override():
    left = Identity("melee1h", "topor", "opiekunczy", "", False)
    right = Identity("melee1h", "piesc niebios", "demoniczny", "", False)
    result, notes = RULES.combine_identity(left, right)
    assert not notes
    assert result is not None
    assert result.prefix == "przyjacielski"
    reverse, reverse_notes = RULES.combine_identity(right, left)
    assert not reverse_notes
    assert reverse is not None
    assert reverse.prefix == "przyjacielski"


def test_legendary_cannot_mix_with_normal():
    a = item(Identity("head", "helm", "", "", True), "Dobry", 1)
    b = item(Identity("head", "kask", "", "", False), "Dobry", 1)
    assert combine_items(a, b, RULES) is None


def test_database_roundtrip():
    with tempfile.TemporaryDirectory() as tmp:
        db = Database(Path(tmp) / "test.db")
        profile = db.upsert_profile("Tester", "R21PL", "Moria")
        ident = Identity("head", "helm", "krwawy", "kary", False)
        source = item(ident, "Dobry", 2, "Dobry Krwawy Hełm Kary (+2)")
        import_id = db.import_inventory(profile, [source], {"source_file": "x.html", "source_hash": "x", "parsed_count": 1, "warnings": []})
        loaded = db.active_items(profile)
        assert len(loaded) == 1
        assert loaded[0].identity == ident
        assert loaded[0].import_id == import_id
        db.close()


def test_real_exports_parse_when_available():
    for filename, expected in [("/mnt/data/20.06.html", 107), ("/mnt/data/22.06.html", 209)]:
        path = Path(filename)
        if path.exists():
            items, meta = parse_armory_html(path, RULES)
            assert len(items) == expected
            assert meta["declared_count"] == expected
            assert not meta["warnings"]


def test_missing_planner_returns_actionable_plan_when_export_available():
    path = Path("/mnt/data/20.06.html")
    if not path.exists():
        return
    items, _ = parse_armory_html(path, RULES)
    target = Identity("head", "helm", "krwawy", "kary", False)
    plans = Planner(RULES).find_plans(target, items, max_depth=3, top_k=2, timeout_seconds=12, allow_missing=True)
    assert plans
    assert plans[0].missing_count >= 0


def test_inventory_display_filters_stack_like_spreadsheet_filters():
    from display_filters import ALL, EMPTY, LEGENDARY_ONLY, NORMAL_ONLY, inventory_filter_values, item_matches

    normal = item(Identity("head", "helm", "krwawy", "kary", False), "Dobry", 2, "Dobry Krwawy Hełm Kary (+2)")
    normal.shelf = "19. Głowa"
    legendary = item(Identity("head", "kask", "", "", True), "Doskonały", 1, "Legendarny Doskonały Kask (+1)")
    legendary.shelf = "18. Legendy"
    values = inventory_filter_values([normal, legendary], RULES)

    assert "Krwawy" in values["prefix"]
    assert EMPTY in values["prefix"]
    assert item_matches(normal, RULES, prefix="Krwawy", suffix="Kary", legendary=NORMAL_ONLY)
    assert not item_matches(normal, RULES, prefix="Krwawy", legendary=LEGENDARY_ONLY)
    assert item_matches(legendary, RULES, prefix=EMPTY, suffix=EMPTY, legendary=LEGENDARY_ONLY)
    assert item_matches(normal, RULES, search="hełm kary", category="Hełm", quality="Dobry", upgrade="+2")
    assert not item_matches(normal, RULES, search="hełm kary", shelf="18. Legendy")
    assert item_matches(normal, RULES, category=ALL)


def test_direct_craft_result_filters_use_result_affixes_and_legendary_status():
    from display_filters import LEGENDARY_ONLY, NORMAL_ONLY, craft_filter_values, craft_matches

    left = item(Identity("head", "helm", "krwawy", "kary", False), "Dobry", 2, "Dobry Krwawy Hełm Kary (+2)")
    right = item(Identity("head", "kask", "krwawy", "kary", False), "Dobry", 3, "Dobry Krwawy Kask Kary (+3)")
    option = combine_items(left, right, RULES)
    assert option is not None
    values = craft_filter_values([option], RULES)
    assert "Krwawy" in values["prefix"]
    assert "Kary" in values["suffix"]
    assert craft_matches(option, RULES, prefix="Krwawy", suffix="Kary", legendary=NORMAL_ONLY, quality="Dobry", upgrade="+2")
    assert not craft_matches(option, RULES, legendary=LEGENDARY_ONLY)
    assert craft_matches(option, RULES, search=option.left.full_name)


def test_prefix_filter_merges_grammatical_variants_and_matches_all_forms():
    from display_filters import craft_filter_values, craft_matches

    head_left = item(Identity("head", "helm", "krwawy", "kary", False), "Dobry", 2, "Dobry Krwawy Hełm Kary (+2)")
    head_right = item(Identity("head", "kask", "krwawy", "kary", False), "Dobry", 2, "Dobry Krwawy Kask Kary (+2)")
    pants_left = item(Identity("pants", "spodnie", "krwawe", "narkomana", False), "Dobry", 2, "Dobre Krwawe Spodnie Narkomana (+2)")
    pants_right = item(Identity("pants", "szorty", "krwawe", "narkomana", False), "Dobry", 2, "Dobre Krwawe Szorty Narkomana (+2)")
    head = combine_items(head_left, head_right, RULES)
    pants = combine_items(pants_left, pants_right, RULES)
    assert head is not None and pants is not None
    values = craft_filter_values([head, pants], RULES)
    assert values["prefix"].count("Krwawy") == 1
    assert "Krwawe" not in values["prefix"]
    assert craft_matches(head, RULES, prefix="Krwawy")
    assert craft_matches(pants, RULES, prefix="Krwawy")


def test_result_names_use_base_grammar():
    assert RULES.identity_display(Identity("pants", "spodnica", "krwawe", "narkomana", False)) == "Krwawa Spódnica Narkomana"
    assert RULES.identity_display(Identity("pants", "spodnie", "krwawe", "narkomana", True)) == "Legendarne Krwawe Spodnie Narkomana"
    assert RULES.identity_display(Identity("melee1h", "piesc niebios", "lekki", "trafienia", False)) == "Lekka Pięść Niebios Trafienia"
    assert RULES.identity_display(Identity("melee1h", "wakizashi", "kasajacy", "wladzy", False)) == "Kasające Wakizashi Władzy"
    assert RULES.identity_display(Identity("head", "bandana", "krwawy", "narkomana", True)) == "Legendarna Krwawa Bandana Narkomana"


def test_target_prefix_label_maps_back_to_category_specific_key():
    assert RULES.prefix_key_from_filter_label("head", "Krwawy") == "krwawy"
    assert RULES.prefix_key_from_filter_label("pants", "Krwawy") == "krwawe"


def test_identical_components_are_valid_even_when_matrix_omits_diagonal():
    for kind, value in [("base", "czapka"), ("prefix", "elegancki"), ("suffix", "podroznika")]:
        result = RULES.component_result("head", kind, value, value)
        assert result is not None
        assert result.value == value
        assert (value, value) in RULES.reverse_component_pairs("head", kind, value)


def test_target_with_omitted_diagonal_can_be_planned_with_missing_items():
    path = Path("/mnt/data/22.06.html")
    if not path.exists():
        return
    items, _ = parse_armory_html(path, RULES)
    target = Identity("head", "czapka", "elegancki", "podroznika", False)
    planner = Planner(RULES)
    plans = planner.find_plans(target, items, max_depth=4, top_k=3, timeout_seconds=30, allow_missing=True)
    assert plans
    assert planner.last_status == "found"
    assert plans[0].missing_count >= 1


def test_planner_accepts_unlimited_timeout_for_simple_existing_target():
    ident = Identity("head", "helm", "krwawy", "kary", False)
    source = item(ident, "Dobry", 2, "Dobry Krwawy Hełm Kary (+2)")
    planner = Planner(RULES)
    plans = planner.find_plans(ident, [source], max_depth=2, timeout_seconds=None)
    assert plans
    assert planner.last_status == "found"
    assert planner.last_timeout_seconds == 0
    assert not planner.last_timed_out


def test_zero_timeout_means_unlimited():
    ident = Identity("head", "helm", "krwawy", "kary", False)
    source = item(ident, "Dobry", 2, "Dobry Krwawy Hełm Kary (+2)")
    planner = Planner(RULES)
    plans = planner.find_plans(ident, [source], max_depth=2, timeout_seconds=0)
    assert plans
    assert planner.last_status == "found"
    assert planner.last_timeout_seconds == 0


def test_category_labels_use_short_names():
    assert RULES.category_label("head") == "Hełm"
    assert RULES.category_label("torso") == "Zbroja"
    assert RULES.category_label("pants") == "Spodnie"
    assert RULES.category_label("rings") == "Pierścień"
    assert RULES.category_label("amulets") == "Amulet"


def test_planner_depth_is_capped_at_five_and_reports_progress():
    ident = Identity("head", "helm", "krwawy", "kary", False)
    source = item(ident, "Dobry", 2, "Dobry Krwawy Hełm Kary (+2)")
    events = []
    planner = Planner(RULES)
    plans = planner.find_plans(
        ident,
        [source],
        max_depth=99,
        timeout_seconds=5,
        progress_callback=events.append,
    )
    assert plans
    assert planner.last_depth == 5
    assert any(event.get("kind") == "start" for event in events)
    assert any(event.get("kind") == "complete" for event in events)


def test_depth_one_means_one_craft_for_missing_plan():
    target = Identity("head", "helm", "krwawy", "kary", False)
    planner = Planner(RULES)
    plans = planner.find_plans(
        target,
        [],
        max_depth=1,
        top_k=2,
        timeout_seconds=10,
        allow_missing=True,
    )
    assert plans
    assert plans[0].step_count == 1
    assert plans[0].missing_count == 2


def test_missing_only_planner_rejects_trivial_target_plus_target_loop():
    target = Identity("head", "kask", "smiercionosny", "prekognicji", False)
    planner = Planner(RULES)
    plans = planner.find_plans(
        target,
        [],
        max_depth=1,
        top_k=10,
        timeout_seconds=10,
        allow_missing=True,
    )
    assert all(
        not (
            len(plan.steps) == 1
            and plan.steps[0].left == target
            and plan.steps[0].right == target
            and len(plan.missing) == 2
            and all(missing == target for missing in plan.missing)
        )
        for plan in plans
    )


def test_self_upgrade_recipe_stays_available_with_one_owned_copy():
    target = Identity("head", "kask", "smiercionosny", "prekognicji", False)
    owned = item(target, "Zwykły", 1, "Śmiercionośny Kask Prekognicji (+1)")
    planner = Planner(RULES)
    plans = planner.find_plans(
        target,
        [owned],
        max_depth=1,
        top_k=10,
        timeout_seconds=10,
        allow_missing=True,
        minimum_level=quality_level("Zwykły", 2),
    )
    assert plans
    assert any(
        len(plan.steps) == 1
        and plan.steps[0].left == target
        and plan.steps[0].right == target
        and len(plan.missing) == 1
        for plan in plans
    )
