import json

import pytest

from smartwallet.model import Category, Transaction
from smartwallet.persistence import (
    DEFAULT_ROOT_NAME,
    SCHEMA_VERSION,
    deserialize_budget,
    load_budget,
    save_budget,
    serialize_budget,
)


def test_roundtrip_save_and_load(tmp_path) -> None:
    root = Category(name="Root")
    root.add(Transaction(name="Salary", amount=500.0))
    root.add(Transaction(name="Rent", amount=-200.0))

    target = tmp_path / "budget.json"
    save_budget(root, path=target)

    loaded = load_budget(path=target)

    assert loaded.get_balance() == root.get_balance()
    assert loaded.to_dict() == root.to_dict()


def test_load_missing_file_returns_root(tmp_path) -> None:
    target = tmp_path / "missing.json"
    root = load_budget(path=target)

    assert root.name == DEFAULT_ROOT_NAME
    assert root.children == []


def test_load_invalid_json_raises_error(tmp_path) -> None:
    target = tmp_path / "invalid.json"
    target.write_text("{not-json}", encoding="utf-8")

    with pytest.raises(ValueError, match="Invalid JSON"):
        load_budget(path=target)


def test_unsupported_schema_version_raises_error() -> None:
    payload = {"schema_version": 99, "data": {"type": "category"}}

    with pytest.raises(ValueError, match="Unsupported schema version"):
        deserialize_budget(payload)


def test_serialize_includes_schema_version() -> None:
    root = Category(name="Root")
    payload = serialize_budget(root)

    assert payload["schema_version"] == SCHEMA_VERSION
    assert payload["data"]["type"] == "category"


def test_deserialize_roundtrip_payload() -> None:
    root = Category(name="Root")
    root.add(Transaction(name="Salary", amount=100.0))

    payload = serialize_budget(root)
    raw = json.loads(json.dumps(payload))

    restored = deserialize_budget(raw)
    assert restored.to_dict() == root.to_dict()


def test_deserialize_missing_data_payload() -> None:
    payload = {"schema_version": SCHEMA_VERSION, "data": "not-a-dict"}

    with pytest.raises(ValueError, match="Missing or invalid data payload"):
        deserialize_budget(payload)


def test_deserialize_root_must_be_category() -> None:
    payload = {
        "schema_version": SCHEMA_VERSION,
        "data": {
            "type": "transaction",
            "id": "abc",
            "name": "Salary",
            "created_at": "2024-01-01",
            "amount": 100.0,
            "transaction_date": "2024-01-01",
        },
    }

    with pytest.raises(ValueError, match="Root component must be a category"):
        deserialize_budget(payload)


def test_deserialize_component_missing_required_fields() -> None:
    payload = {
        "schema_version": SCHEMA_VERSION,
        "data": {"type": "category", "id": "abc", "name": None, "created_at": "x"},
    }

    with pytest.raises(ValueError, match="missing required fields"):
        deserialize_budget(payload)


def test_deserialize_invalid_transaction_fields() -> None:
    payload = {
        "schema_version": SCHEMA_VERSION,
        "data": {
            "type": "category",
            "id": "root",
            "name": "Root",
            "created_at": "2024-01-01",
            "children": [
                {
                    "type": "transaction",
                    "id": "t1",
                    "name": "Bad",
                    "created_at": "2024-01-01",
                    "amount": "not-a-number",
                    "transaction_date": "2024-01-01",
                },
            ],
        },
    }

    with pytest.raises(ValueError, match="Serialized transaction is invalid"):
        deserialize_budget(payload)


def test_deserialize_invalid_children_type() -> None:
    payload = {
        "schema_version": SCHEMA_VERSION,
        "data": {
            "type": "category",
            "id": "root",
            "name": "Root",
            "created_at": "2024-01-01",
            "children": "not-a-list",
        },
    }

    with pytest.raises(ValueError, match="Serialized category children are invalid"):
        deserialize_budget(payload)


def test_deserialize_child_not_a_dict() -> None:
    payload = {
        "schema_version": SCHEMA_VERSION,
        "data": {
            "type": "category",
            "id": "root",
            "name": "Root",
            "created_at": "2024-01-01",
            "children": ["not-a-dict"],
        },
    }

    with pytest.raises(ValueError, match="Serialized child component is invalid"):
        deserialize_budget(payload)


def test_deserialize_unknown_component_type() -> None:
    payload = {
        "schema_version": SCHEMA_VERSION,
        "data": {
            "type": "unknown",
            "id": "abc",
            "name": "Mystery",
            "created_at": "2024-01-01",
        },
    }

    with pytest.raises(ValueError, match="Unknown component type"):
        deserialize_budget(payload)
