"""Tests for xmldev repair — deterministic fixes."""
from __future__ import annotations

import json
import pytest

from xmldev import Xmldev
from xmldev.schema import SchemaLoader


def make_xd():
    return Xmldev(fuzzy_profile="balanced")


# ---------------------------------------------------------------------------
# PRD canonical example: fullname → name, "twenty five" → 25
# ---------------------------------------------------------------------------

PERSON_SCHEMA = json.dumps({
    "root": {
        "name": "person",
        "attrs": {"id": {"name": "id", "type": "int", "required": True}},
        "children": [
            {
                "spec": {"name": "name", "text_type": "string", "aliases": ["fullname", "full-name"]},
                "min_occurs": 1,
                "order_index": 0,
            },
            {
                "spec": {"name": "age", "text_type": "int", "default": 0},
                "min_occurs": 0,
                "order_index": 1,
            },
        ],
        "order_enforced": True,
    }
})


def test_alias_rename():
    """fullname → name via alias mapping."""
    xd = make_xd()
    schema = SchemaLoader.load(PERSON_SCHEMA)
    xml = b'<person id="1"><fullname>Alice</fullname></person>'
    result = xd.validate_and_fix(xml, schema)
    assert "<name>" in result["fixed_xml"]
    assert "<fullname>" not in result["fixed_xml"]
    renames = [p for p in result["patches"] if p["type"] == "rename"]
    assert any("fullname" in p["notes"] for p in renames)


def test_fuzzy_rename():
    """Tag with a single-char typo → name via fuzzy match (permissive profile)."""
    xd = Xmldev(fuzzy_profile="permissive")
    schema = SchemaLoader.load(PERSON_SCHEMA)
    xml = b'<person id="1"><neme>Bob</neme></person>'
    result = xd.validate_and_fix(xml, schema)
    assert "<name>" in result["fixed_xml"]


def test_text_type_coercion_words_to_int():
    """'twenty five' should coerce to 25 with low confidence."""
    xd = make_xd()
    schema = SchemaLoader.load(PERSON_SCHEMA)
    xml = b'<person id="1"><name>Bob</name><age>twenty five</age></person>'
    result = xd.validate_and_fix(xml, schema)
    coercions = [p for p in result["patches"] if p["type"] == "coerce"]
    assert any(p["replacement"] == "25" for p in coercions)
    # "twenty five" coercion is low confidence
    low_conf = [p for p in coercions if p["confidence"] < 0.9]
    assert low_conf


def test_text_type_coercion_string_to_int():
    """'42' string where int is expected should pass (already valid)."""
    xd = make_xd()
    schema = SchemaLoader.load(PERSON_SCHEMA)
    xml = b'<person id="1"><name>Bob</name><age>42</age></person>'
    result = xd.validate_and_fix(xml, schema)
    assert result["ok"] is True
    assert not result["patches"]


def test_default_insertion():
    """Missing age (min_occurs=0) with default=0 is not required — no auto-insert."""
    xd = make_xd()
    schema = SchemaLoader.load(PERSON_SCHEMA)
    xml = b'<person id="1"><name>Bob</name></person>'
    result = xd.validate_and_fix(xml, schema)
    # age is optional (min=0) so no missing-required violation
    assert result["ok"] is True


def test_required_child_with_default():
    """Required element (min=1) with a default IS auto-inserted."""
    schema_dict = {
        "root": {
            "name": "order",
            "children": [
                {
                    "spec": {"name": "status", "text_type": "string", "default": "pending"},
                    "min_occurs": 1,
                    "order_index": 0,
                }
            ],
        }
    }
    xd = make_xd()
    schema = SchemaLoader.load(json.dumps(schema_dict))
    xml = b"<order></order>"
    result = xd.validate_and_fix(xml, schema)
    assert "<status>" in result["fixed_xml"]
    insertions = [p for p in result["patches"] if p["type"] == "insert"]
    assert insertions


def test_move_to_extension():
    """Unknown element moved to <extensions> per default policy."""
    xd = make_xd()
    schema = SchemaLoader.load(PERSON_SCHEMA)
    xml = b'<person id="1"><name>Bob</name><unknown_field>xyz</unknown_field></person>'
    result = xd.validate_and_fix(xml, schema)
    moves = [p for p in result["patches"] if p["type"] == "move"]
    assert moves
    assert "<extensions>" in result["fixed_xml"] or "extensions" in result["fixed_xml"]


def test_drop_unknown():
    """Unknown element dropped per drop policy."""
    schema_dict = {
        "root": {"name": "book"},
        "global": {"allow_unknown": "drop"},
    }
    xd = make_xd()
    schema = SchemaLoader.load(json.dumps(schema_dict))
    xml = b"<book><unknown>data</unknown></book>"
    result = xd.validate_and_fix(xml, schema)
    deletes = [p for p in result["patches"] if p["type"] == "delete"]
    assert deletes
    assert "<unknown>" not in result["fixed_xml"]


def test_attr_type_coercion():
    """Attribute 'id' is int but given as string 'abc' — fails coercion."""
    xd = make_xd()
    schema = SchemaLoader.load(PERSON_SCHEMA)
    xml = b'<person id="abc"><name>Bob</name></person>'
    result = xd.validate_and_fix(xml, schema)
    # 'abc' can't be coerced to int, so it stays broken
    # but the rest of the doc should be ok
    assert result["patches"] is not None


def test_child_reorder():
    """Children in wrong order should be reordered."""
    schema_dict = {
        "root": {
            "name": "person",
            "children": [
                {"spec": {"name": "name"}, "min_occurs": 1, "order_index": 0},
                {"spec": {"name": "age"}, "min_occurs": 0, "order_index": 1},
            ],
            "order_enforced": True,
        }
    }
    xd = make_xd()
    schema = SchemaLoader.load(json.dumps(schema_dict))
    # age comes before name — wrong order
    xml = b"<person><age>30</age><name>Bob</name></person>"
    result = xd.validate_and_fix(xml, schema)
    reorders = [p for p in result["patches"] if p["type"] == "reorder"]
    assert reorders
    # In the fixed XML, name should appear before age
    fixed = result["fixed_xml"]
    assert fixed.index("<name>") < fixed.index("<age>")


def test_max_renames_cap():
    """Rename cap should stop renames at max_renames_per_doc."""
    schema_dict = {
        "root": {
            "name": "root",
            "children": [
                {"spec": {"name": "item", "aliases": ["itm"]}, "min_occurs": 0, "max_occurs": "unbounded"},
            ],
        },
        "global": {"fuzzy": {"max_renames_per_doc": 2}},
    }
    xd = make_xd()
    schema = SchemaLoader.load(json.dumps(schema_dict))
    # 5 elements that need renaming
    xml = b"<root><itm>1</itm><itm>2</itm><itm>3</itm><itm>4</itm><itm>5</itm></root>"
    result = xd.validate_and_fix(xml, schema)
    renames = [p for p in result["patches"] if p["type"] == "rename"]
    # Should be capped at 2
    assert len(renames) <= 2


def test_well_formed_no_patches():
    """Valid XML produces no patches and ok=True."""
    xd = make_xd()
    schema = SchemaLoader.load(PERSON_SCHEMA)
    xml = b'<person id="1"><name>Alice</name><age>30</age></person>'
    result = xd.validate_and_fix(xml, schema)
    assert result["ok"] is True
    assert result["patches"] == []
