from dataclasses import dataclass

import pytest
from fasthtml.components import Label, Input
from myutils.observable import collect_return_values

from myfasthtml.core.bindings import (
  BindingsManager,
  Binding,
  DetectionMode,
  UpdateMode,
  BooleanConverter
)


@dataclass
class Data:
  value: str = "Hello World"


@pytest.fixture(autouse=True)
def reset_binding_manager():
  BindingsManager.reset()


@pytest.fixture()
def data():
  return Data()


def test_i_can_register_a_binding(data):
  binding = Binding(data, "value")
  
  assert binding.id is not None
  assert binding.data is data
  assert binding.data_attr == 'value'


def test_i_can_register_a_binding_with_default_attr(data):
  binding = Binding(data)
  
  assert binding.id is not None
  assert binding.data is data
  assert binding.data_attr == 'value'


def test_i_can_retrieve_a_registered_binding(data):
  elt = Label("hello", id="label_id")
  binding = Binding(data).bind_ft(elt, name="label_name")
  
  assert BindingsManager.get_binding(binding.id) is binding


def test_i_can_reset_bindings(data):
  elt = Label("hello", id="label_id")
  Binding(data).bind_ft(elt, name="label_name")
  assert len(BindingsManager.bindings) != 0
  
  BindingsManager.reset()
  assert len(BindingsManager.bindings) == 0


def test_i_can_bind_an_element_to_a_binding(data):
  elt = Label("hello", id="label_id")
  Binding(data).bind_ft(elt, name="label_name")
  
  data.value = "new value"
  
  assert elt.children[0] == "new value"
  assert elt.attrs["hx-swap-oob"] == "true"
  assert elt.attrs["id"] == "label_id"


def test_i_can_bind_an_element_attr_to_a_binding(data):
  elt = Input(value="some value", id="input_id")
  
  Binding(data).bind_ft(elt, name="input_name", attr="value")
  
  data.value = "new value"
  
  assert elt.attrs["value"] == "new value"
  assert elt.attrs["hx-swap-oob"] == "true"
  assert elt.attrs["id"] == "input_id"


def test_bound_element_has_an_id():
  elt = Label("hello")
  assert elt.attrs.get("id", None) is None
  
  Binding(Data()).bind_ft(elt, name="label_name")
  assert elt.attrs.get("id", None) is not None


def test_i_can_collect_updates_values(data):
  elt = Label("hello")
  Binding(data).bind_ft(elt, name="label_name")
  
  data.value = "new value"
  collected = collect_return_values(data)
  assert collected == [elt]
  
  # a second time to ensure no side effect
  data.value = "another value"
  collected = collect_return_values(data)
  assert collected == [elt]


def test_i_can_react_to_value_change(data):
  elt = Input(name="input_elt", value="hello")
  binding = Binding(data).bind_ft(elt, name="input_elt", attr="value")
  
  res = binding.update({"input_elt": "new value"})
  
  assert len(res) == 1


def test_i_do_not_react_to_other_value_change(data):
  elt = Input(name="input_elt", value="hello")
  binding = Binding(data).bind_ft(elt, name="input_elt", attr="value")
  
  res = binding.update({"other_input_elt": "new value"})
  
  assert res is None


def test_i_can_react_to_attr_presence(data):
  elt = Input(name="input_elt", type="checkbox")
  binding = Binding(data).bind_ft(
    elt,
    name="input_elt",
    attr="checked",
    detection_mode=DetectionMode.AttributePresence
  )
  
  res = binding.update({"checked": "true"})
  
  assert len(res) == 1


def test_i_can_react_to_attr_non_presence(data):
  elt = Input(name="input_elt", type="checkbox")
  binding = Binding(data).bind_ft(
    elt,
    name="input_elt",
    attr="checked",
    detection_mode=DetectionMode.AttributePresence
  )
  
  res = binding.update({})
  
  assert len(res) == 1


def test_i_can_create_a_binding_without_activation(data):
  """
  A binding created without calling bind_ft should not be active.
  """
  binding = Binding(data, "value")
  
  assert binding._is_active is False
  assert binding.ft is None
  assert binding.ft_name is None
  assert binding.ft_attr is None
  assert BindingsManager.get_binding(binding.id) is None


def test_i_can_activate_binding_via_bind_ft(data):
  """
  Calling bind_ft should automatically activate the binding.
  """
  elt = Label("hello", id="label_id")
  binding = Binding(data, "value")
  
  binding.bind_ft(elt, name="label_name")
  
  assert binding._is_active is True
  assert binding.ft is elt
  assert binding.ft_name == "label_name"
  assert BindingsManager.get_binding(binding.id) is binding


def test_i_cannot_notify_when_not_active(data):
  """
  A non-active binding should not update the UI when data changes.
  """
  elt = Label("hello", id="label_id")
  binding = Binding(data, "value")
  binding.ft = elt
  binding.ft_name = "label_name"
  
  # Change data without activating the binding
  result = binding.notify("old", "new")
  
  assert result is None
  assert elt.children[0] == "hello"  # Should not have changed


def test_i_can_deactivate_a_binding(data):
  """
  Deactivating a binding should clean up observers and unregister it.
  """
  elt = Label("hello", id="label_id")
  binding = Binding(data, "value").bind_ft(elt, name="label_name")
  
  assert binding._is_active is True
  assert BindingsManager.get_binding(binding.id) is binding
  
  binding.deactivate()
  
  assert binding._is_active is False
  assert BindingsManager.get_binding(binding.id) is None


def test_i_can_reactivate_a_binding(data):
  """
  After deactivation, a binding can be reactivated by calling bind_ft again.
  """
  elt1 = Label("hello", id="label_id_1")
  binding = Binding(data, "value").bind_ft(elt1, name="label_name_1")
  
  binding.deactivate()
  assert binding._is_active is False
  
  elt2 = Label("world", id="label_id_2")
  binding.bind_ft(elt2, name="label_name_2")
  
  assert binding._is_active is True
  assert binding.ft is elt2
  assert binding.ft_name == "label_name_2"


def test_bind_ft_deactivates_before_reconfiguring(data):
  """
  Calling bind_ft on an active binding should deactivate it first,
  then reconfigure and reactivate.
  """
  elt1 = Label("hello", id="label_id_1")
  elt2 = Label("world", id="label_id_2")
  
  binding = Binding(data, "value").bind_ft(elt1, name="label_name_1")
  
  # Change data to verify old binding works
  data.value = "updated"
  assert elt1.children[0] == "updated"
  
  # Reconfigure with new element
  binding.bind_ft(elt2, name="label_name_2")
  
  # Change data again
  data.value = "final"
  
  # Old element should not update
  assert elt1.children[0] == "updated"
  
  # New element should update
  assert elt2.children[0] == "final"


def test_deactivate_can_be_called_multiple_times(data):
  """
  Calling deactivate multiple times should be safe (idempotent).
  """
  elt = Label("hello", id="label_id")
  binding = Binding(data, "value").bind_ft(elt, name="label_name")
  
  binding.deactivate()
  binding.deactivate()  # Should not raise an error
  binding.deactivate()  # Should not raise an error
  
  assert binding._is_active is False


def test_i_cannot_activate_without_configuration(data):
  """
  Calling activate directly without proper configuration should raise ValueError.
  """
  binding = Binding(data, "value")
  
  with pytest.raises(ValueError, match="ft element is required"):
    binding.activate()


def test_activation_validates_strategies(data):
  """
  Activation should fail if detection/update strategies are not initialized.
  """
  elt = Label("hello", id="label_id")
  binding = Binding(data, "value")
  binding.ft = elt
  binding.ft_name = "label_name"
  
  with pytest.raises(ValueError, match="detection strategy not initialized"):
    binding.activate()


def test_i_can_chain_bind_ft_calls(data):
  """
  bind_ft should return self for method chaining.
  """
  elt = Label("hello", id="label_id")
  
  binding = Binding(data, "value").bind_ft(elt, name="label_name")
  
  assert isinstance(binding, Binding)
  assert binding._is_active is True


def test_bind_ft_updates_optional_parameters(data):
  """
  bind_ft should update optional parameters if provided.
  """
  elt = Input(name="input_elt", type="checkbox")
  
  binding = Binding(data, "value")
  
  binding.bind_ft(
    elt,
    name="input_elt",
    attr="checked",
    data_converter=BooleanConverter(),
    detection_mode=DetectionMode.AttributePresence,
    update_mode=UpdateMode.AttributePresence
  )
  
  assert binding.detection_mode == DetectionMode.AttributePresence
  assert binding.update_mode == UpdateMode.AttributePresence
  assert isinstance(binding.data_converter, BooleanConverter)


def test_deactivated_binding_does_not_update_on_data_change(data):
  """
  After deactivation, changes to data should not update the UI element.
  """
  elt = Label("hello", id="label_id")
  binding = Binding(data, "value").bind_ft(elt, name="label_name")
  
  # Verify it works when active
  data.value = "first update"
  assert elt.children[0] == "first update"
  
  # Deactivate
  binding.deactivate()
  
  # Change data - element should NOT update
  data.value = "second update"
  assert elt.children[0] == "first update"


def test_multiple_bindings_can_coexist(data):
  """
  Multiple bindings can be created and managed independently.
  """
  elt1 = Label("hello", id="label_id_1")
  elt2 = Input(value="world", id="input_id_2")
  
  binding1 = Binding(data, "value").bind_ft(elt1, name="label_name")
  binding2 = Binding(data, "value").bind_ft(elt2, name="input_name", attr="value")
  
  assert len(BindingsManager.bindings) == 2
  assert binding1._is_active is True
  assert binding2._is_active is True
  
  # Change data - both should update
  data.value = "updated"
  assert elt1.children[0] == "updated"
  assert elt2.attrs["value"] == "updated"
  
  # Deactivate one
  binding1.deactivate()
  assert len(BindingsManager.bindings) == 1
  
  # Change data - only binding2 should update
  data.value = "final"
  assert elt1.children[0] == "updated"  # Not changed
  assert elt2.attrs["value"] == "final"  # Changed


def test_i_cannot_bind_when_htmx_post_already_set(data):
  elt = Input(name="input_elt", hx_post="/some/url")
  binding = Binding(data, "value")
  
  with pytest.raises(ValueError, match="htmx post already set on input"):
    binding.bind_ft(elt, name="label_name")
