"""
Exercise 2: build a mini Pydantic (medium)

Implement parse(cls, data) that turns a dict into a dataclass instance, using the type hints of `cls`
(typing.get_type_hints, typing.get_origin, typing.get_args), and collects ALL errors before failing:

  - int / float / str / bool fields: accept values of that type; coerce numeric strings for int/float
    ("30" -> 30, "2.5" -> 2.5). bool accepts only True/False. Anything else is an error "<path>: expected <type>".
  - Optional[X]: None is allowed; otherwise parse as X.
  - List[X]: must be a list; parse each element with path "<field>[<index>]".
  - Nested dataclass: parse recursively with path "<field>.<subfield>".
  - Missing required fields (no default): error "<path>: missing". Fields with defaults may be omitted.
  - If any errors, raise ValidationError(errors) where errors is the list of messages in field order.
(Python 3.9 compatible: use typing.List / typing.Optional.)
"""

import dataclasses
import typing
from dataclasses import dataclass, field
from typing import List, Optional


class ValidationError(Exception):
    def __init__(self, errors):
        super().__init__("; ".join(errors))
        self.errors = errors


def parse(cls, data):
    return cls(**data)  # TODO


# --- self-check: do not modify below this line ---
@dataclass
class Address:
    city: str
    zip: str


@dataclass
class User:
    name: str
    age: int
    score: float = 0.0
    active: bool = True
    nickname: Optional[str] = None
    tags: List[str] = field(default_factory=list)
    address: Optional[Address] = None


def errors_of(data):
    try:
        parse(User, data)
        return []
    except ValidationError as e:
        return e.errors

checks = []
u = parse(User, {"name": "Ana", "age": "30", "score": "2.5", "tags": ["a", "b"], "address": {"city": "Tashkent", "zip": "100000"}})
checks.append(("coerces and nests", (u.age, u.score, u.tags, u.address), (30, 2.5, ["a", "b"], Address("Tashkent", "100000"))))
checks.append(("defaults", (u.active, u.nickname), (True, None)))
checks.append(("missing field", errors_of({"name": "Ana"}), ["age: missing"]))
checks.append(("collects all errors", errors_of({"name": 5, "age": "x", "active": "yes", "tags": ["ok", 3], "address": {"city": "T"}}),
               ["name: expected str", "age: expected int", "active: expected bool", "tags[1]: expected str", "address.zip: missing"]))
checks.append(("optional None ok", parse(User, {"name": "B", "age": 1, "nickname": None}).nickname, None))

passed = 0
for label, actual, expected in checks:
    ok = actual == expected
    passed += ok
    print(f"{'PASS' if ok else 'FAIL'}: {label} (got {actual!r}, expected {expected!r})")

print("PASS" if passed == len(checks) else "FAIL")
