"""Step definitions for UKO Layer 3 Technology Vocabularies -- base models and vocabs.

All steps use the suffix "for uko_l3" to avoid AmbiguousStep errors.
"""

from __future__ import annotations

from datetime import UTC, datetime
from typing import Any

from behave import given, then
from pydantic import ValidationError

from cleveragents.acms.uko.layer3_java import JAVA_VOCABULARY, JavaClass
from cleveragents.acms.uko.layer3_py import (
    PYTHON_VOCABULARY,
    PythonClass,
)
from cleveragents.acms.uko.layer3_rs import RUST_VOCABULARY, RustStruct
from cleveragents.acms.uko.layer3_ts import TYPESCRIPT_VOCABULARY, TypeScriptClass
from cleveragents.acms.uko.vocabulary import (
    Layer2Dependency,
    ProvenanceInfo,
    UKOClass,
    UKOProperty,
    UKOVocabulary,
)

__all__: list[str] = []

# -- ProvenanceInfo steps --


@given('a provenance info with resource "{resource}" and path "{path}" for uko_l3')
def step_given_provenance_info(context: Any, resource: str, path: str) -> None:
    context.provenance = ProvenanceInfo(
        source_resource=resource,
        source_path=path,
    )


@given(
    'a ranged provenance info with resource "{resource}" and path '
    '"{path}" and range "{source_range}" for uko_l3'
)
def step_given_provenance_info_with_range(
    context: Any, resource: str, path: str, source_range: str
) -> None:
    context.provenance = ProvenanceInfo(
        source_resource=resource,
        source_path=path,
        source_range=source_range,
    )


@then('the provenance source_resource should be "{expected}" for uko_l3')
def step_then_provenance_resource(context: Any, expected: str) -> None:
    assert context.provenance.source_resource == expected


@then('the provenance source_path should be "{expected}" for uko_l3')
def step_then_provenance_path(context: Any, expected: str) -> None:
    assert context.provenance.source_path == expected


@then("the provenance is_current should be true for uko_l3")
def step_then_provenance_is_current(context: Any) -> None:
    assert context.provenance.is_current is True


@then("the provenance valid_from should be a UTC datetime for uko_l3")
def step_then_provenance_valid_from_utc(context: Any) -> None:
    vf = context.provenance.valid_from
    assert isinstance(vf, datetime)
    assert vf.tzinfo is not None
    assert vf.tzinfo == UTC


@then('the provenance source_range should be "{expected}" for uko_l3')
def step_then_provenance_source_range(context: Any, expected: str) -> None:
    assert context.provenance.source_range == expected


@then(
    "creating a provenance info with empty source_resource "
    "should raise ValueError for uko_l3"
)
def step_then_provenance_empty_resource(context: Any) -> None:
    try:
        ProvenanceInfo(source_resource="", source_path="some.py")
        msg = "Expected ValueError"
        raise AssertionError(msg)
    except ValueError:
        pass


@then(
    "creating a provenance info with whitespace source_resource "
    "should raise ValueError for uko_l3"
)
def step_then_provenance_whitespace_resource(context: Any) -> None:
    try:
        ProvenanceInfo(source_resource="   ", source_path="some.py")
        msg = "Expected ValueError"
        raise AssertionError(msg)
    except ValueError:
        pass


@then(
    "creating a provenance info with empty source_path "
    "should raise ValueError for uko_l3"
)
def step_then_provenance_empty_path(context: Any) -> None:
    try:
        ProvenanceInfo(source_resource="res-01", source_path="")
        msg = "Expected ValueError"
        raise AssertionError(msg)
    except ValueError:
        pass


@then(
    "creating a provenance info with whitespace source_path "
    "should raise ValueError for uko_l3"
)
def step_then_provenance_whitespace_path(context: Any) -> None:
    try:
        ProvenanceInfo(source_resource="res-01", source_path="   ")
        msg = "Expected ValueError"
        raise AssertionError(msg)
    except ValueError:
        pass


@then("modifying provenance is_current should raise an error for uko_l3")
def step_then_provenance_immutable(context: Any) -> None:
    try:
        context.provenance.is_current = False
        msg = "Expected immutability error"
        raise AssertionError(msg)
    except (TypeError, AttributeError, ValidationError):
        pass


# -- UKOClass steps --


@given('a UKO class with uri "{uri}" and label "{label}" for uko_l3')
def step_given_uko_class(context: Any, uri: str, label: str) -> None:
    context.uko_class = UKOClass(uri=uri, label=label)


@then('the UKO class uri should be "{expected}" for uko_l3')
def step_then_uko_class_uri(context: Any, expected: str) -> None:
    assert context.uko_class.uri == expected


@then('the UKO class label should be "{expected}" for uko_l3')
def step_then_uko_class_label(context: Any, expected: str) -> None:
    assert context.uko_class.label == expected


@then("the UKO class subclass_of should be empty for uko_l3")
def step_then_uko_class_subclass_empty(context: Any) -> None:
    assert context.uko_class.subclass_of == ()


@then("creating a UKO class with empty uri should raise ValueError for uko_l3")
def step_then_uko_class_empty_uri(context: Any) -> None:
    try:
        UKOClass(uri="", label="X")
        msg = "Expected ValueError"
        raise AssertionError(msg)
    except ValueError:
        pass


@then("creating a UKO class with whitespace label should raise ValueError for uko_l3")
def step_then_uko_class_whitespace_label(context: Any) -> None:
    try:
        UKOClass(uri="https://example.com/X", label="   ")
        msg = "Expected ValueError"
        raise AssertionError(msg)
    except ValueError:
        pass


@then("modifying UKO class label should raise an error for uko_l3")
def step_then_uko_class_immutable(context: Any) -> None:
    try:
        context.uko_class.label = "mutated"
        msg = "Expected immutability error"
        raise AssertionError(msg)
    except (TypeError, AttributeError, ValidationError):
        pass


# -- UKOClass provenance steps --


@given('a UKO class with provenance resource "{resource}" and path "{path}" for uko_l3')
def step_given_uko_class_with_provenance(
    context: Any, resource: str, path: str
) -> None:
    context.uko_class = UKOClass(
        uri="https://example.com/WithProv",
        label="WithProv",
        provenance=ProvenanceInfo(
            source_resource=resource,
            source_path=path,
        ),
    )


@then('the UKO class provenance source_resource should be "{expected}" for uko_l3')
def step_then_uko_class_prov_resource(context: Any, expected: str) -> None:
    assert context.uko_class.provenance is not None
    assert context.uko_class.provenance.source_resource == expected


@then('the UKO class provenance source_path should be "{expected}" for uko_l3')
def step_then_uko_class_prov_path(context: Any, expected: str) -> None:
    assert context.uko_class.provenance is not None
    assert context.uko_class.provenance.source_path == expected


@then("the UKO class provenance should be None for uko_l3")
def step_then_uko_class_prov_none(context: Any) -> None:
    assert context.uko_class.provenance is None


# -- UKOProperty steps --


@given('a UKO property with uri "{uri}" and label "{label}" for uko_l3')
def step_given_uko_property(context: Any, uri: str, label: str) -> None:
    context.uko_property = UKOProperty(uri=uri, label=label)


@then('the UKO property uri should be "{expected}" for uko_l3')
def step_then_uko_property_uri(context: Any, expected: str) -> None:
    assert context.uko_property.uri == expected


@then("the UKO property is_object_property should be true for uko_l3")
def step_then_uko_property_is_object(context: Any) -> None:
    assert context.uko_property.is_object_property is True


@then("creating a UKO property with empty uri should raise ValueError for uko_l3")
def step_then_uko_property_empty_uri(context: Any) -> None:
    try:
        UKOProperty(uri="", label="X")
        msg = "Expected ValueError"
        raise AssertionError(msg)
    except ValueError:
        pass


# -- Layer2Dependency steps --


@given('a layer2 dependency with uri "{uri}" and prefix "{prefix}" for uko_l3')
def step_given_layer2_dep(context: Any, uri: str, prefix: str) -> None:
    context.layer2_dep = Layer2Dependency(uri=uri, prefix=prefix)


@then("the layer2 dependency layer should be {expected:d} for uko_l3")
def step_then_layer2_dep_layer(context: Any, expected: int) -> None:
    assert context.layer2_dep.layer == expected


@then('the layer2 dependency prefix should be "{expected}" for uko_l3')
def step_then_layer2_dep_prefix(context: Any, expected: str) -> None:
    assert context.layer2_dep.prefix == expected


# -- UKOVocabulary steps --


@given(
    'a UKO vocabulary with namespace "{ns}" and prefix "{prefix}" '
    'and label "{label}" for uko_l3'
)
def step_given_uko_vocab(context: Any, ns: str, prefix: str, label: str) -> None:
    context.uko_vocab = UKOVocabulary(namespace=ns, prefix=prefix, label=label)


@then("the vocabulary layer should be {expected:d} for uko_l3")
def step_then_vocab_layer(context: Any, expected: int) -> None:
    assert context.uko_vocab.layer == expected


@then('the vocabulary prefix should be "{expected}" for uko_l3')
def step_then_vocab_prefix(context: Any, expected: str) -> None:
    assert context.uko_vocab.prefix == expected


# -- Python vocabulary steps --


@then('the Python vocabulary namespace should be "{expected}" for uko_l3')
def step_then_py_namespace(context: Any, expected: str) -> None:
    assert PYTHON_VOCABULARY.namespace == expected


@then('the Python vocabulary prefix should be "{expected}" for uko_l3')
def step_then_py_prefix(context: Any, expected: str) -> None:
    assert PYTHON_VOCABULARY.prefix == expected


@then("the Python vocabulary layer should be {expected:d} for uko_l3")
def step_then_py_layer(context: Any, expected: int) -> None:
    assert PYTHON_VOCABULARY.layer == expected


@then("the Python vocabulary should have {count:d} classes for uko_l3")
def step_then_py_class_count(context: Any, count: int) -> None:
    assert len(PYTHON_VOCABULARY.classes) == count


@then('the Python vocabulary should contain class "{name}" for uko_l3')
def step_then_py_has_class(context: Any, name: str) -> None:
    labels = tuple(c.label for c in PYTHON_VOCABULARY.classes)
    assert name in labels, f"{name!r} not in {labels}"


@then("the Python vocabulary should have {count:d} properties for uko_l3")
def step_then_py_property_count(context: Any, count: int) -> None:
    assert len(PYTHON_VOCABULARY.properties) == count


@then('the Python vocabulary should contain property "{name}" for uko_l3')
def step_then_py_has_property(context: Any, name: str) -> None:
    labels = tuple(p.label for p in PYTHON_VOCABULARY.properties)
    assert name in labels, f"{name!r} not in {labels}"


@then('the Python vocabulary parent_prefixes should contain "{prefix}" for uko_l3')
def step_then_py_parent_prefix(context: Any, prefix: str) -> None:
    assert prefix in PYTHON_VOCABULARY.parent_prefixes


@then('the PythonClass subclass_of should contain "{uri}" for uko_l3')
def step_then_python_class_subclass(context: Any, uri: str) -> None:
    assert uri in PythonClass.subclass_of, f"{uri!r} not in {PythonClass.subclass_of}"


@then("the Python vocabulary should have {count:d} inserted levels for uko_l3")
def step_then_py_inserted_count(context: Any, count: int) -> None:
    assert len(PYTHON_VOCABULARY.inserted_levels) == count


@then('the Python vocabulary inserted levels should include "{name}" for uko_l3')
def step_then_py_inserted_name(context: Any, name: str) -> None:
    names = tuple(n for n, _ in PYTHON_VOCABULARY.inserted_levels)
    assert name in names, f"{name!r} not in {names}"


# -- TypeScript vocabulary steps --


@then('the TypeScript vocabulary namespace should be "{expected}" for uko_l3')
def step_then_ts_namespace(context: Any, expected: str) -> None:
    assert TYPESCRIPT_VOCABULARY.namespace == expected


@then('the TypeScript vocabulary prefix should be "{expected}" for uko_l3')
def step_then_ts_prefix(context: Any, expected: str) -> None:
    assert TYPESCRIPT_VOCABULARY.prefix == expected


@then("the TypeScript vocabulary layer should be {expected:d} for uko_l3")
def step_then_ts_layer(context: Any, expected: int) -> None:
    assert TYPESCRIPT_VOCABULARY.layer == expected


@then("the TypeScript vocabulary should have {count:d} classes for uko_l3")
def step_then_ts_class_count(context: Any, count: int) -> None:
    assert len(TYPESCRIPT_VOCABULARY.classes) == count


@then('the TypeScript vocabulary should contain class "{name}" for uko_l3')
def step_then_ts_has_class(context: Any, name: str) -> None:
    labels = tuple(c.label for c in TYPESCRIPT_VOCABULARY.classes)
    assert name in labels, f"{name!r} not in {labels}"


@then("the TypeScript vocabulary should have {count:d} properties for uko_l3")
def step_then_ts_property_count(context: Any, count: int) -> None:
    assert len(TYPESCRIPT_VOCABULARY.properties) == count


@then('the TypeScript vocabulary should contain property "{name}" for uko_l3')
def step_then_ts_has_property(context: Any, name: str) -> None:
    labels = tuple(p.label for p in TYPESCRIPT_VOCABULARY.properties)
    assert name in labels, f"{name!r} not in {labels}"


@then('the TypeScript vocabulary parent_prefixes should contain "{prefix}" for uko_l3')
def step_then_ts_parent_prefix(context: Any, prefix: str) -> None:
    assert prefix in TYPESCRIPT_VOCABULARY.parent_prefixes


@then("the TypeScript vocabulary should have {count:d} inserted levels for uko_l3")
def step_then_ts_inserted_count(context: Any, count: int) -> None:
    assert len(TYPESCRIPT_VOCABULARY.inserted_levels) == count


# -- Rust vocabulary steps --


@then('the Rust vocabulary namespace should be "{expected}" for uko_l3')
def step_then_rs_namespace(context: Any, expected: str) -> None:
    assert RUST_VOCABULARY.namespace == expected


@then('the Rust vocabulary prefix should be "{expected}" for uko_l3')
def step_then_rs_prefix(context: Any, expected: str) -> None:
    assert RUST_VOCABULARY.prefix == expected


@then("the Rust vocabulary layer should be {expected:d} for uko_l3")
def step_then_rs_layer(context: Any, expected: int) -> None:
    assert RUST_VOCABULARY.layer == expected


@then("the Rust vocabulary should have {count:d} classes for uko_l3")
def step_then_rs_class_count(context: Any, count: int) -> None:
    assert len(RUST_VOCABULARY.classes) == count


@then('the Rust vocabulary should contain class "{name}" for uko_l3')
def step_then_rs_has_class(context: Any, name: str) -> None:
    labels = tuple(c.label for c in RUST_VOCABULARY.classes)
    assert name in labels, f"{name!r} not in {labels}"


@then("the Rust vocabulary should have {count:d} properties for uko_l3")
def step_then_rs_property_count(context: Any, count: int) -> None:
    assert len(RUST_VOCABULARY.properties) == count


@then('the Rust vocabulary should contain property "{name}" for uko_l3')
def step_then_rs_has_property(context: Any, name: str) -> None:
    labels = tuple(p.label for p in RUST_VOCABULARY.properties)
    assert name in labels, f"{name!r} not in {labels}"


@then('the Rust vocabulary parent_prefixes should contain "{prefix}" for uko_l3')
def step_then_rs_parent_prefix(context: Any, prefix: str) -> None:
    assert prefix in RUST_VOCABULARY.parent_prefixes


@then("the Rust vocabulary should have {count:d} inserted levels for uko_l3")
def step_then_rs_inserted_count(context: Any, count: int) -> None:
    assert len(RUST_VOCABULARY.inserted_levels) == count


@then('the RustStruct subclass_of should contain "{uri}" for uko_l3')
def step_then_rs_struct_subclass(context: Any, uri: str) -> None:
    assert uri in RustStruct.subclass_of, f"{uri!r} not in {RustStruct.subclass_of}"


# -- Java vocabulary steps --


@then('the Java vocabulary namespace should be "{expected}" for uko_l3')
def step_then_java_namespace(context: Any, expected: str) -> None:
    assert JAVA_VOCABULARY.namespace == expected


@then('the Java vocabulary prefix should be "{expected}" for uko_l3')
def step_then_java_prefix(context: Any, expected: str) -> None:
    assert JAVA_VOCABULARY.prefix == expected


@then("the Java vocabulary layer should be {expected:d} for uko_l3")
def step_then_java_layer(context: Any, expected: int) -> None:
    assert JAVA_VOCABULARY.layer == expected


@then("the Java vocabulary should have {count:d} classes for uko_l3")
def step_then_java_class_count(context: Any, count: int) -> None:
    assert len(JAVA_VOCABULARY.classes) == count


@then('the Java vocabulary should contain class "{name}" for uko_l3')
def step_then_java_has_class(context: Any, name: str) -> None:
    labels = tuple(c.label for c in JAVA_VOCABULARY.classes)
    assert name in labels, f"{name!r} not in {labels}"


@then("the Java vocabulary should have {count:d} properties for uko_l3")
def step_then_java_property_count(context: Any, count: int) -> None:
    assert len(JAVA_VOCABULARY.properties) == count


@then('the Java vocabulary should contain property "{name}" for uko_l3')
def step_then_java_has_property(context: Any, name: str) -> None:
    labels = tuple(p.label for p in JAVA_VOCABULARY.properties)
    assert name in labels, f"{name!r} not in {labels}"


@then('the Java vocabulary parent_prefixes should contain "{prefix}" for uko_l3')
def step_then_java_parent_prefix(context: Any, prefix: str) -> None:
    assert prefix in JAVA_VOCABULARY.parent_prefixes


@then("the Java vocabulary should have {count:d} inserted levels for uko_l3")
def step_then_java_inserted_count(context: Any, count: int) -> None:
    assert len(JAVA_VOCABULARY.inserted_levels) == count


@then('the JavaClass subclass_of should contain "{uri}" for uko_l3')
def step_then_java_class_subclass(context: Any, uri: str) -> None:
    assert uri in JavaClass.subclass_of, f"{uri!r} not in {JavaClass.subclass_of}"


# -- TypeScript subclass_of step --


@then('the TypeScriptClass subclass_of should contain "{uri}" for uko_l3')
def step_then_ts_class_subclass(context: Any, uri: str) -> None:
    assert uri in TypeScriptClass.subclass_of, (
        f"{uri!r} not in {TypeScriptClass.subclass_of}"
    )
