import enum
from abc import ABC, abstractmethod
from collections.abc import Mapping
from unittest import mock

import dagster as dg
import pytest
from dagster import (
    AssetExecutionContext,
    ConfigurableResource,
    DagsterInstance,
    InitResourceContext,
)
from dagster._check import CheckError
from dagster._config.pythonic_config import (
    ConfigurableResourceFactory,
    infer_schema_from_config_class,
)
from dagster._utils.cached_method import cached_method
from pydantic import (
    Field as PyField,
    ValidationError,
    create_model,
)


def test_basic_structured_resource():
    out_txt = []

    class WriterResource(dg.ConfigurableResource):
        prefix: str

        def output(self, text: str) -> None:
            out_txt.append(f"{self.prefix}{text}")

    @dg.op
    def hello_world_op(writer: WriterResource):
        writer.output("hello, world!")

    @dg.job(resource_defs={"writer": WriterResource(prefix="")})
    def no_prefix_job():
        hello_world_op()

    assert no_prefix_job.execute_in_process().success
    assert out_txt == ["hello, world!"]

    out_txt.clear()

    @dg.job(resource_defs={"writer": WriterResource(prefix="greeting: ")})
    def prefix_job():
        hello_world_op()

    assert prefix_job.execute_in_process().success
    assert out_txt == ["greeting: hello, world!"]


def test_basic_structured_resource_assets() -> None:
    out_txt = []

    class WriterResource(dg.ConfigurableResource):
        prefix: str

        def output(self, text: str) -> None:
            out_txt.append(f"{self.prefix}{text}")

    @dg.asset
    def hello_world_asset(writer: WriterResource):
        writer.output("hello, world!")

    defs = dg.Definitions(
        assets=[hello_world_asset],
        resources={"writer": WriterResource(prefix="greeting: ")},
    )

    assert defs.resolve_implicit_global_asset_job_def().execute_in_process().success
    assert out_txt == ["greeting: hello, world!"]


def test_invalid_config() -> None:
    class MyResource(dg.ConfigurableResource):
        foo: int

    with pytest.raises(
        ValidationError,
    ):
        MyResource(foo="why")  # type: ignore


def test_caching_within_resource():
    called = {"greeting": 0, "get_introduction": 0}

    from functools import cached_property

    class GreetingResource(dg.ConfigurableResource):
        name: str

        @cached_property
        def greeting(self) -> str:
            called["greeting"] += 1
            return f"Hello, {self.name}"

        # Custom decorator which caches an instance method
        @cached_method
        def get_introduction(self, verbose: bool) -> str:
            called["get_introduction"] += 1
            return f"My name is {self.name}" if verbose else f"I'm {self.name}"

    @dg.op
    def hello_world_op(greeting: GreetingResource):
        assert greeting.greeting == "Hello, Dagster"
        assert greeting.get_introduction(verbose=True) == "My name is Dagster"
        assert greeting.get_introduction(verbose=False) == "I'm Dagster"

    @dg.op
    def another_op(greeting: GreetingResource):
        assert greeting.greeting == "Hello, Dagster"
        assert greeting.get_introduction(verbose=True) == "My name is Dagster"
        assert greeting.get_introduction(verbose=False) == "I'm Dagster"

    @dg.job(resource_defs={"greeting": GreetingResource(name="Dagster")})
    def hello_world_job():
        hello_world_op()
        another_op()

    assert hello_world_job.execute_in_process().success

    # Each should only be called once, because of the caching
    assert called["greeting"] == 1
    assert called["get_introduction"] == 2

    called = {"greeting": 0, "get_introduction": 0}

    @dg.asset
    def hello_world_asset(greeting: GreetingResource):
        assert greeting.greeting == "Hello, Dagster"
        assert greeting.get_introduction(verbose=True) == "My name is Dagster"
        assert greeting.get_introduction(verbose=False) == "I'm Dagster"
        return greeting.greeting

    @dg.asset
    def another_asset(greeting: GreetingResource, hello_world_asset):
        assert hello_world_asset == "Hello, Dagster"
        assert greeting.greeting == "Hello, Dagster"
        assert greeting.get_introduction(verbose=True) == "My name is Dagster"
        assert greeting.get_introduction(verbose=False) == "I'm Dagster"

    assert dg.materialize(
        [hello_world_asset, another_asset],
        resources={"greeting": GreetingResource(name="Dagster")},
    ).success

    assert called["greeting"] == 1
    assert called["get_introduction"] == 2


def test_abc_resource():
    out_txt = []

    class Writer(dg.ConfigurableResource, ABC):
        @abstractmethod
        def output(self, text: str) -> None:
            pass

    class PrefixedWriterResource(Writer):
        prefix: str

        def output(self, text: str) -> None:
            out_txt.append(f"{self.prefix}{text}")

    class RepetitiveWriterResource(Writer):
        repetitions: int

        def output(self, text: str) -> None:
            out_txt.append(f"{text} " * self.repetitions)

    @dg.op
    def hello_world_op(writer: Writer):
        writer.output("hello, world!")

    # Can't instantiate abstract class
    with pytest.raises(TypeError):
        Writer()  # pyright: ignore[reportAbstractUsage]

    @dg.job(resource_defs={"writer": PrefixedWriterResource(prefix="greeting: ")})
    def prefixed_job():
        hello_world_op()

    assert prefixed_job.execute_in_process().success
    assert out_txt == ["greeting: hello, world!"]

    out_txt.clear()

    @dg.job(resource_defs={"writer": RepetitiveWriterResource(repetitions=3)})
    def repetitive_writer_job():
        hello_world_op()

    assert repetitive_writer_job.execute_in_process().success
    assert out_txt == ["hello, world! " * 3]


def test_yield_in_resource_function():
    called = []

    class ResourceWithCleanup(ConfigurableResourceFactory[bool]):
        idx: int

        def create_resource(self, context):  # pyright: ignore[reportIncompatibleMethodOverride]
            called.append(f"creation_{self.idx}")
            yield True
            called.append(f"cleanup_{self.idx}")

    @dg.op
    def check_resource_created(
        resource_with_cleanup_1: dg.ResourceParam[bool],
        resource_with_cleanup_2: dg.ResourceParam[bool],
    ):
        assert resource_with_cleanup_1 is True
        assert resource_with_cleanup_2 is True
        called.append("op")

    @dg.job(
        resource_defs={
            "resource_with_cleanup_1": ResourceWithCleanup(idx=1),
            "resource_with_cleanup_2": ResourceWithCleanup(idx=2),
        }
    )
    def the_job():
        check_resource_created()

    assert the_job.execute_in_process().success

    assert called == ["creation_1", "creation_2", "op", "cleanup_2", "cleanup_1"]


def test_migration_attach_bare_object_to_context() -> None:
    executed = {}

    class MyClient:
        def foo(self) -> str:
            return "foo"

    class MyClientResource(dg.ConfigurableResource, dg.IAttachDifferentObjectToOpContext):
        def get_client(self) -> MyClient:
            return MyClient()

        def get_object_to_set_on_execution_context(self) -> MyClient:
            return self.get_client()

    @dg.asset(required_resource_keys={"my_client"})
    def uses_client_asset_unmigrated(context) -> str:
        assert context.resources.my_client
        assert context.resources.my_client.foo() == "foo"
        executed["unmigrated"] = True
        return "foo"

    @dg.asset
    def uses_client_asset_migrated(my_client: MyClientResource) -> str:
        assert my_client
        assert my_client.get_client().foo() == "foo"
        executed["migrated"] = True
        return "foo"

    defs = dg.Definitions(
        assets=[uses_client_asset_migrated, uses_client_asset_unmigrated],
        resources={"my_client": MyClientResource()},
    )

    asset_job = defs.resolve_implicit_global_asset_job_def()
    assert asset_job
    assert asset_job.execute_in_process().success
    assert executed["unmigrated"]
    assert executed["migrated"]


class AnIOManagerImplementation(dg.IOManager):
    def __init__(self, a_config_value: str):
        self.a_config_value = a_config_value

    def load_input(self, _):  # pyright: ignore[reportIncompatibleMethodOverride]
        pass

    def handle_output(self, _, obj):  # pyright: ignore[reportIncompatibleMethodOverride]
        pass


def test_io_manager_adapter():
    @dg.io_manager(config_schema={"a_config_value": str})
    def an_io_manager(context: InitResourceContext) -> AnIOManagerImplementation:
        return AnIOManagerImplementation(context.resource_config["a_config_value"])

    class AdapterForIOManager(dg.ConfigurableLegacyIOManagerAdapter):
        a_config_value: str

        @property
        def wrapped_io_manager(self) -> dg.IOManagerDefinition:
            return an_io_manager

    executed = {}

    @dg.asset
    def an_asset(context: AssetExecutionContext):
        assert context.resources.io_manager.a_config_value == "passed-in-configured"
        executed["yes"] = True

    defs = dg.Definitions(
        assets=[an_asset],
        resources={"io_manager": AdapterForIOManager(a_config_value="passed-in-configured")},
    )
    defs.resolve_implicit_global_asset_job_def().execute_in_process()

    assert executed["yes"]


def test_io_manager_factory_class():
    # now test without the adapter
    class AnIOManagerFactory(dg.ConfigurableIOManagerFactory):
        a_config_value: str

        def create_io_manager(self, _) -> dg.IOManager:  # pyright: ignore[reportIncompatibleMethodOverride]
            """Implement as one would implement a @io_manager decorator function."""
            return AnIOManagerImplementation(self.a_config_value)

    executed = {}

    @dg.asset
    def another_asset(context: AssetExecutionContext):
        assert context.resources.io_manager.a_config_value == "passed-in-factory"
        executed["yes"] = True

    defs = dg.Definitions(
        assets=[another_asset],
        resources={"io_manager": AnIOManagerFactory(a_config_value="passed-in-factory")},
    )
    defs.resolve_implicit_global_asset_job_def().execute_in_process()

    assert executed["yes"]


def test_structured_resource_runtime_config():
    out_txt = []

    class WriterResource(dg.ConfigurableResource):
        prefix: str

        def output(self, text: str) -> None:
            out_txt.append(f"{self.prefix}{text}")

    @dg.asset
    def hello_world_asset(writer: WriterResource):
        writer.output("hello, world!")

    defs = dg.Definitions(
        assets=[hello_world_asset],
        resources={"writer": WriterResource.configure_at_launch()},
    )

    assert (
        defs.resolve_implicit_global_asset_job_def()
        .execute_in_process({"resources": {"writer": {"config": {"prefix": ""}}}})
        .success
    )
    assert out_txt == ["hello, world!"]

    out_txt.clear()

    assert (
        defs.resolve_implicit_global_asset_job_def()
        .execute_in_process({"resources": {"writer": {"config": {"prefix": "greeting: "}}}})
        .success
    )
    assert out_txt == ["greeting: hello, world!"]


def test_runtime_config_run_config_obj():
    # Use RunConfig to specify resource config
    # in a structured format at runtime rather than using a dict

    out_txt = []

    class WriterResource(dg.ConfigurableResource):
        prefix: str

        def output(self, text: str) -> None:
            out_txt.append(f"{self.prefix}{text}")

    @dg.asset
    def hello_world_asset(writer: WriterResource):
        writer.output("hello, world!")

    defs = dg.Definitions(
        assets=[hello_world_asset],
        resources={"writer": WriterResource.configure_at_launch()},
    )

    assert (
        defs.resolve_implicit_global_asset_job_def()
        .execute_in_process(dg.RunConfig(resources={"writer": WriterResource(prefix="greeting: ")}))
        .success
    )
    assert out_txt == ["greeting: hello, world!"]


def test_basic_enum_override_with_resource_instance() -> None:
    class BasicEnum(enum.Enum):
        A = "a_value"
        B = "b_value"

    setup_executed = {}

    class MyResource(dg.ConfigurableResource):
        my_enum: BasicEnum

        def setup_for_execution(self, context: InitResourceContext) -> None:
            setup_executed["yes"] = True
            assert context.resource_config["my_enum"] in [
                BasicEnum.A.value,
                BasicEnum.B.value,
            ]

    @dg.asset
    def asset_with_resource(context, my_resource: MyResource):
        return my_resource.my_enum.value

    result_one = dg.materialize(
        [asset_with_resource],
        resources={"my_resource": MyResource(my_enum=BasicEnum.A)},
    )
    assert result_one.success
    assert result_one.output_for_node("asset_with_resource") == "a_value"
    assert setup_executed["yes"]

    setup_executed.clear()

    result_two = dg.materialize(
        [asset_with_resource],
        resources={"my_resource": MyResource(my_enum=BasicEnum.A)},
        run_config={"resources": {"my_resource": {"config": {"my_enum": "B"}}}},
    )

    assert result_two.success
    assert result_two.output_for_node("asset_with_resource") == "b_value"
    assert setup_executed["yes"]


def test_basic_enum_override_with_resource_configured_at_launch() -> None:
    class AnotherEnum(enum.Enum):
        A = "a_value"
        B = "b_value"

    class MyResource(dg.ConfigurableResource):
        my_enum: AnotherEnum

    @dg.asset
    def asset_with_resource(context, my_resource: MyResource):
        return my_resource.my_enum.value

    result_one = dg.materialize(
        [asset_with_resource],
        resources={"my_resource": MyResource.configure_at_launch()},
        run_config={"resources": {"my_resource": {"config": {"my_enum": "B"}}}},
    )

    assert result_one.success
    assert result_one.output_for_node("asset_with_resource") == "b_value"

    result_two = dg.materialize(
        [asset_with_resource],
        resources={"my_resource": MyResource.configure_at_launch(my_enum=AnotherEnum.A)},
        run_config={"resources": {"my_resource": {"config": {"my_enum": "B"}}}},
    )

    assert result_two.success
    assert result_two.output_for_node("asset_with_resource") == "b_value"


def test_resources_which_return():
    class StringResource(ConfigurableResourceFactory[str]):
        a_string: str

        def create_resource(self, context) -> str:
            return self.a_string

    class MyResource(dg.ConfigurableResource):
        string_from_resource: dg.ResourceDependency[str]

    completed = {}

    @dg.asset
    def my_asset(my_resource: MyResource):
        assert my_resource.string_from_resource == "foo"
        completed["yes"] = True

    str_resource = StringResource(a_string="foo")
    my_resource = MyResource(string_from_resource=str_resource)

    defs = dg.Definitions(
        assets=[my_asset],
        resources={
            "my_resource": my_resource,
        },
    )

    assert defs.resolve_implicit_global_asset_job_def().execute_in_process().success
    assert completed["yes"]

    str_resource_partial = StringResource.configure_at_launch()
    my_resource = MyResource(string_from_resource=str_resource_partial)  # pyright: ignore[reportArgumentType]

    defs = dg.Definitions(
        assets=[my_asset],
        resources={
            "str_resource_partial": str_resource_partial,
            "my_resource": my_resource,
        },
    )

    assert (
        defs.resolve_implicit_global_asset_job_def()
        .execute_in_process(
            {
                "resources": {
                    "str_resource_partial": {
                        "config": {
                            "a_string": "foo",
                        },
                    }
                }
            }
        )
        .success
    )
    assert completed["yes"]


def test_nested_config_class() -> None:
    # Validate that we can nest Config classes in a pythonic resource

    class User(dg.Config):
        name: str
        age: int

    class UsersResource(dg.ConfigurableResource):
        users: list[User]

    executed = {}

    @dg.asset
    def an_asset(users_resource: UsersResource):
        assert len(users_resource.users) == 2
        assert users_resource.users[0].name == "Bob"
        assert users_resource.users[0].age == 25
        assert users_resource.users[1].name == "Alice"
        assert users_resource.users[1].age == 30

        executed["yes"] = True

    defs = dg.Definitions(
        assets=[an_asset],
        resources={
            "users_resource": UsersResource(
                users=[
                    User(name="Bob", age=25),
                    User(name="Alice", age=30),
                ]
            )
        },
    )

    assert defs.resolve_implicit_global_asset_job_def().execute_in_process().success
    assert executed["yes"]


# https://github.com/dagster-io/dagster/issues/27223
@pytest.mark.parametrize("child_resource_fields_all_have_default_values", [True, False])
def test_nested_config_class_with_runtime_config(
    child_resource_fields_all_have_default_values: bool,
) -> None:
    # Type hinting a dynamically-generated Pydantic model is impossible:
    # https://stackoverflow.com/q/78838473
    ChildResource = create_model(
        "ChildResource",
        date=(str, "2025-01-20" if child_resource_fields_all_have_default_values else ...),
        __base__=ConfigurableResource,
    )

    class ParentResource(dg.ConfigurableResource):
        child: ChildResource  # pyright: ignore[reportInvalidTypeForm]

    @dg.asset
    def test_asset(
        child: ChildResource,  # pyright: ignore[reportInvalidTypeForm]
        parent: ParentResource,
    ) -> None:
        assert child.date == "2025-01-21"
        assert parent.child.date == "2025-01-21"

    child = ChildResource.configure_at_launch()
    dg.materialize(
        [test_asset],
        resources={
            "child": child,
            "parent": ParentResource.configure_at_launch(child=child),
        },
        run_config={
            "loggers": {"console": {"config": {"log_level": "ERROR"}}},
            "resources": {"child": {"config": {"date": "2025-01-21"}}},
        },
    )


def test_using_enum_simple() -> None:
    executed = {}

    class SimpleEnum(enum.Enum):
        FOO = "foo"
        BAR = "bar"

    class MyResource(dg.ConfigurableResource):
        an_enum: SimpleEnum

    @dg.asset
    def an_asset(my_resource: MyResource):
        assert my_resource.an_enum == SimpleEnum.FOO
        executed["yes"] = True

    defs = dg.Definitions(
        assets=[an_asset],
        resources={
            "my_resource": MyResource(
                an_enum=SimpleEnum.FOO,
            )
        },
    )

    assert defs.resolve_implicit_global_asset_job_def().execute_in_process().success
    assert executed["yes"]
    executed.clear()

    defs = dg.Definitions(
        assets=[an_asset],
        resources={
            "my_resource": MyResource.configure_at_launch(),
        },
    )

    assert (
        defs.resolve_implicit_global_asset_job_def()
        .execute_in_process(
            {"resources": {"my_resource": {"config": {"an_enum": SimpleEnum.FOO.name}}}}
        )
        .success
    )
    assert executed["yes"]


def test_using_enum_complex() -> None:
    executed = {}

    class MyEnum(enum.Enum):
        FOO = "foo"
        BAR = "bar"

    class MyResource(dg.ConfigurableResource):
        list_of_enums: list[MyEnum]
        optional_enum: MyEnum | None = None

    @dg.asset
    def an_asset(my_resource: MyResource):
        assert my_resource.optional_enum is None
        assert my_resource.list_of_enums == [MyEnum.FOO, MyEnum.BAR]
        executed["yes"] = True

    defs = dg.Definitions(
        assets=[an_asset],
        resources={
            "my_resource": MyResource(
                list_of_enums=[MyEnum.FOO, MyEnum.BAR],
            )
        },
    )

    assert defs.resolve_implicit_global_asset_job_def().execute_in_process().success
    assert executed["yes"]
    executed.clear()


def test_resource_defs_on_asset() -> None:
    executed = {}

    class MyResource(dg.ConfigurableResource):
        a_str: str

    @dg.asset(resource_defs={"my_resource": MyResource(a_str="foo")})
    def an_asset(my_resource: MyResource):
        assert my_resource.a_str == "foo"
        executed["yes"] = True

    defs = dg.Definitions(
        assets=[an_asset],
    )
    defs.resolve_implicit_global_asset_job_def().execute_in_process()

    assert executed["yes"]

    # Cannot specify both required_resource_keys and resources as args
    with pytest.raises(CheckError):

        @dg.asset(required_resource_keys={"my_other_resource"})
        def an_other_asset(my_resource: MyResource):
            pass


def test_extending_resource() -> None:
    executed = {}

    class BaseResource(dg.ConfigurableResource):
        a_str: str = "bar"
        an_int: int = 1

    class ExtendingResource(BaseResource):
        a_float: float = 1.0

    @dg.op
    def hello_world_op(writer: ExtendingResource):
        assert writer.a_str == "foo"
        assert writer.an_int == 1
        assert writer.a_float == 1.0
        executed["yes"] = True

    @dg.job(resource_defs={"writer": ExtendingResource(a_str="foo")})
    def no_prefix_job() -> None:
        hello_world_op()

    assert no_prefix_job.execute_in_process().success
    assert executed["yes"]


def test_extending_resource_nesting() -> None:
    executed = {}

    class NestedResource(dg.ConfigurableResource):
        a_str: str

    class BaseResource(dg.ConfigurableResource):
        nested: NestedResource
        a_str: str = "bar"
        an_int: int = 1

    class ExtendingResource(BaseResource):
        a_float: float = 1.0

    @dg.asset
    def an_asset(writer: ExtendingResource):
        assert writer.a_str == "foo"
        assert writer.nested.a_str == "baz"
        assert writer.an_int == 1
        assert writer.a_float == 1.0
        executed["yes"] = True

    defs = dg.Definitions(
        assets=[an_asset],
        resources={"writer": ExtendingResource(a_str="foo", nested=NestedResource(a_str="baz"))},
    )
    assert defs.resolve_implicit_global_asset_job_def().execute_in_process().success

    assert executed["yes"]
    executed.clear()

    nested_defer = NestedResource.configure_at_launch()
    defs = dg.Definitions(
        assets=[an_asset],
        resources={
            "nested_deferred": nested_defer,
            "writer": ExtendingResource(a_str="foo", nested=nested_defer),
        },
    )
    assert (
        defs.resolve_implicit_global_asset_job_def()
        .execute_in_process(
            run_config={"resources": {"nested_deferred": {"config": {"a_str": "baz"}}}}
        )
        .success
    )

    assert executed["yes"]


def test_execute_in_process() -> None:
    out_txt = []

    class WriterResource(dg.ConfigurableResource):
        prefix: str

        def output(self, text: str) -> None:
            out_txt.append(f"{self.prefix}{text}")

    @dg.op
    def hello_world_op(writer: WriterResource):
        writer.output("hello, world!")

    @dg.job
    def hello_world_job() -> None:
        hello_world_op()

    with pytest.raises(
        dg.DagsterInvalidDefinitionError,
        match="resource with key 'writer' required by op 'hello_world_op' was not provided",
    ):
        hello_world_job.execute_in_process()

    assert not out_txt

    # Bind resource as part of calling execute_in_process
    assert hello_world_job.execute_in_process(
        resources={"writer": WriterResource(prefix="msg: ")}
    ).success
    assert out_txt == ["msg: hello, world!"]


def test_aliased_field_structured_resource():
    out_txt = []

    class WriterResource(dg.ConfigurableResource):
        prefix_: str = PyField(..., alias="prefix")

        def output(self, text: str) -> None:
            out_txt.append(f"{self.prefix_}{text}")

    @dg.op
    def hello_world_op(writer: WriterResource):
        writer.output("hello, world!")

    @dg.job(resource_defs={"writer": WriterResource(prefix="")})
    def no_prefix_job():
        hello_world_op()

    assert no_prefix_job.execute_in_process().success
    assert out_txt == ["hello, world!"]

    out_txt.clear()

    @dg.job(resource_defs={"writer": WriterResource(prefix="greeting: ")})
    def prefix_job():
        hello_world_op()

    assert prefix_job.execute_in_process().success
    assert out_txt == ["greeting: hello, world!"]

    out_txt.clear()

    @dg.job(resource_defs={"writer": WriterResource.configure_at_launch()})
    def prefix_job_at_runtime():
        hello_world_op()

    assert prefix_job_at_runtime.execute_in_process(
        {"resources": {"writer": {"config": {"prefix": "runtime: "}}}}
    ).success
    assert out_txt == ["runtime: hello, world!"]


def test_from_resource_context_and_to_config_field() -> None:
    class StringResource(ConfigurableResourceFactory[str]):
        a_string: str

        def create_resource(self, context) -> str:
            return self.a_string + "bar"

    @dg.resource(config_schema=StringResource.to_config_schema())
    def string_resource_function_style(context: InitResourceContext) -> str:
        return StringResource.from_resource_context(context)

    assert (
        string_resource_function_style(dg.build_init_resource_context({"a_string": "foo"}))
        == "foobar"
    )


def test_from_resource_context_and_to_config_field_complex() -> None:
    class MyComplexConfigResource(dg.ConfigurableResource):
        a_string: str
        a_list_of_ints: list[int]
        a_map_of_lists_of_maps_of_floats: Mapping[str, list[Mapping[str, float]]]

    @dg.resource(config_schema=MyComplexConfigResource.to_config_schema())
    def complex_config_resource_function_style(
        context: InitResourceContext,
    ) -> MyComplexConfigResource:
        return MyComplexConfigResource.from_resource_context(context)

    complex_config_resource = complex_config_resource_function_style(
        dg.build_init_resource_context(
            {
                "a_string": "foo",
                "a_list_of_ints": [1, 2, 3],
                "a_map_of_lists_of_maps_of_floats": {
                    "a": [{"b": 1.0}, {"c": 2.0}],
                    "d": [{"e": 3.0}, {"f": 4.0}],
                },
            }
        )
    )
    assert complex_config_resource.a_string == "foo"
    assert complex_config_resource.a_list_of_ints == [1, 2, 3]
    assert complex_config_resource.a_map_of_lists_of_maps_of_floats == {
        "a": [{"b": 1.0}, {"c": 2.0}],
        "d": [{"e": 3.0}, {"f": 4.0}],
    }


def test_from_resource_context_and_to_config_empty() -> None:
    class NoConfigResource(dg.ConfigurableResource[str]):
        def get_string(self) -> str:
            return "foo"

    @dg.resource(config_schema=NoConfigResource.to_config_schema())
    def string_resource_function_style(context: InitResourceContext) -> str:
        return NoConfigResource.from_resource_context(context).get_string()  # type: ignore  # (??)

    assert string_resource_function_style(dg.build_init_resource_context()) == "foo"


def test_context_on_resource_basic() -> None:
    executed = {}

    class ContextUsingResource(dg.ConfigurableResource):
        def access_context(self) -> None:
            self.get_resource_context()

    with pytest.raises(
        CheckError, match=r"Attempted to get context before resource was initialized."
    ):
        ContextUsingResource().access_context()

    # Can access context after binding one
    ContextUsingResource().with_replaced_resource_context(
        dg.build_init_resource_context()
    ).access_context()

    @dg.asset
    def my_test_asset(context_using: ContextUsingResource) -> None:
        context_using.access_context()
        executed["yes"] = True

    defs = dg.Definitions(
        assets=[my_test_asset],
        resources={"context_using": ContextUsingResource()},
    )

    assert defs.resolve_implicit_global_asset_job_def().execute_in_process().success
    assert executed["yes"]


def test_context_on_resource_use_instance() -> None:
    executed = {}

    class OutputDirResource(dg.ConfigurableResource):
        output_dir: str | None = None

        def get_effective_output_dir(self) -> str:
            if self.output_dir:
                return self.output_dir

            context = self.get_resource_context()
            assert context.instance
            return context.instance.storage_directory()

    with pytest.raises(
        CheckError, match=r"Attempted to get context before resource was initialized."
    ):
        OutputDirResource(output_dir=None).get_effective_output_dir()

    with mock.patch(
        "dagster._core.instance.DagsterInstance.storage_directory"
    ) as storage_directory:
        storage_directory.return_value = "/tmp"

        with DagsterInstance.ephemeral() as instance:
            assert (
                OutputDirResource(output_dir=None)
                .with_replaced_resource_context(dg.build_init_resource_context(instance=instance))
                .get_effective_output_dir()
                == "/tmp"
            )

        @dg.asset
        def my_other_output_asset(output_dir: OutputDirResource) -> None:
            assert output_dir.get_effective_output_dir() == "/tmp"
            executed["yes"] = True

        defs = dg.Definitions(
            assets=[my_other_output_asset],
            resources={"output_dir": OutputDirResource()},
        )

        assert defs.resolve_implicit_global_asset_job_def().execute_in_process().success
        assert executed["yes"]


def test_context_on_resource_runtime_config() -> None:
    executed = {}

    class OutputDirResource(dg.ConfigurableResource):
        output_dir: str | None = None

        def get_effective_output_dir(self) -> str:
            if self.output_dir:
                return self.output_dir

            context = self.get_resource_context()
            assert context.instance
            return context.instance.storage_directory()

    with mock.patch(
        "dagster._core.instance.DagsterInstance.storage_directory"
    ) as storage_directory:
        storage_directory.return_value = "/tmp"

        @dg.asset
        def my_other_output_asset(output_dir: OutputDirResource) -> None:
            assert output_dir.get_effective_output_dir() == "/tmp"
            executed["yes"] = True

        defs = dg.Definitions(
            assets=[my_other_output_asset],
            resources={"output_dir": OutputDirResource.configure_at_launch()},
        )

        assert (
            defs.resolve_implicit_global_asset_job_def()
            .execute_in_process(
                run_config={"resources": {"output_dir": {"config": {"output_dir": None}}}}
            )
            .success
        )
        assert executed["yes"]


def test_context_on_resource_nested() -> None:
    executed = {}

    class OutputDirResource(dg.ConfigurableResource):
        output_dir: str | None = None

        def get_effective_output_dir(self) -> str:
            if self.output_dir:
                return self.output_dir

            context = self.get_resource_context()
            assert context.instance
            return context.instance.storage_directory()

    class OutputDirWrapperResource(dg.ConfigurableResource):
        output_dir: OutputDirResource

    with pytest.raises(
        CheckError, match=r"Attempted to get context before resource was initialized."
    ):
        OutputDirWrapperResource(
            output_dir=OutputDirResource(output_dir=None)
        ).output_dir.get_effective_output_dir()

    with mock.patch(
        "dagster._core.instance.DagsterInstance.storage_directory"
    ) as storage_directory:
        storage_directory.return_value = "/tmp"

        @dg.asset
        def my_other_output_asset(wrapper: OutputDirWrapperResource) -> None:
            assert wrapper.output_dir.get_effective_output_dir() == "/tmp"
            executed["yes"] = True

        defs = dg.Definitions(
            assets=[my_other_output_asset],
            resources={"wrapper": OutputDirWrapperResource(output_dir=OutputDirResource())},
        )

        assert defs.resolve_implicit_global_asset_job_def().execute_in_process().success
        assert executed["yes"]


def test_telemetry_custom_resource():
    class MyResource(dg.ConfigurableResource):
        my_value: str

        @classmethod
        def _is_dagster_maintained(cls) -> bool:
            return False

    assert not MyResource(my_value="foo")._is_dagster_maintained()  # noqa: SLF001


def test_telemetry_dagster_resource():
    class MyResource(dg.ConfigurableResource):
        my_value: str

        @classmethod
        def _is_dagster_maintained(cls) -> bool:
            return True

    assert MyResource(my_value="foo")._is_dagster_maintained()  # noqa: SLF001


def test_partial_resource_checks() -> None:
    class IntResource(dg.ConfigurableResource):
        my_int: int

    class StrResource(dg.ConfigurableResource):
        my_str: str

    class MergeResource(dg.ConfigurableResource):
        str_res: StrResource
        int_res: IntResource

    MergeResource(
        str_res=StrResource.configure_at_launch(),
        int_res=IntResource.configure_at_launch(),
    )

    # this should fail but does not https://github.com/dagster-io/dagster/issues/18017
    MergeResource(
        int_res=StrResource.configure_at_launch(),
        str_res=IntResource.configure_at_launch(),
    )


def test_secret_field():
    """Test that is_secret is extracted from json_schema_extra in ConfigurableResource."""

    class MyResource(dg.ConfigurableResource):
        api_key: str = PyField(
            description="API key for authentication",
            json_schema_extra={"dagster__is_secret": True},
        )
        host: str = PyField(description="Host URL")
        password: str = PyField(json_schema_extra={"dagster__is_secret": True})

    # Get the inferred schema
    schema_field = infer_schema_from_config_class(MyResource)

    # The schema should have the fields
    fields_dict = schema_field.config_type.fields  # type: ignore

    # Check that api_key is marked as secret
    assert "api_key" in fields_dict
    assert fields_dict["api_key"].is_secret is True

    # Check that host is not marked as secret
    assert "host" in fields_dict
    assert fields_dict["host"].is_secret is False

    # Check that password is marked as secret
    assert "password" in fields_dict
    assert fields_dict["password"].is_secret is True
