"""
Comprehensive binding tests for all bindable FastHTML components.

This test suite covers:
- Input (text) - already tested
- Checkbox - already tested
- Textarea
- Select (single)
- Select (multiple)
- Range (slider)
- Radio buttons
- Button
- Input with Datalist (combobox)
"""

from dataclasses import dataclass
from typing import Any

import pytest
from fasthtml.components import (
  Input, Label, Textarea, Select, Option, Datalist
)
from fasthtml.fastapp import fast_app

from myfasthtml.controls.helpers import mk
from myfasthtml.core.bindings import Binding, BooleanConverter
from myfasthtml.core.constants import Routes, ROUTE_ROOT
from myfasthtml.test.matcher import matches, AttributeForbidden, AnyValue
from myfasthtml.test.testclient import MyTestClient


@dataclass
class Data:
  value: Any = "hello world"


@dataclass
class NumericData:
  value: int = 50


@dataclass
class BoolData:
  value: bool = True


@dataclass
class ListData:
  value: list = None
  
  def __post_init__(self):
    if self.value is None:
      self.value = []


@pytest.fixture()
def user():
  test_app, rt = fast_app(default_hdrs=False)
  user = MyTestClient(test_app)
  return user


@pytest.fixture()
def rt(user):
  return user.app.route


class TestBindingTextarea:
  """Tests for binding Textarea components."""
  
  def test_i_can_bind_textarea(self):
    data = Data("")
    check_box = Textarea(name="textarea_name")
    
    binding = Binding(data)
    mk.manage_binding(check_box, binding)
    
    # update the content
    res = binding.update({"textarea_name": "Hello world !"})
    expected = [Textarea("Hello world !", name="textarea_name", hx_swap_oob="true")]
    assert matches(res, expected)
  
  def test_i_can_bind_textarea_with_label(self, user, rt):
    """
    Textarea should bind bidirectionally with data.
    Value changes should update the label.
    """
    
    @rt("/")
    def index():
      data = Data("Initial text")
      textarea_elt = Textarea(name="textarea_name")
      label_elt = Label()
      mk.manage_binding(textarea_elt, Binding(data))
      mk.manage_binding(label_elt, Binding(data))
      return textarea_elt, label_elt
    
    user.open("/")
    user.should_see("Initial text")
    
    testable_textarea = user.find_element("textarea")
    testable_textarea.send("New multiline\ntext content")
    user.should_see("New multiline\ntext content")
  
  def test_textarea_append_works_with_binding(self, user, rt):
    """
    Appending text to textarea should trigger binding update.
    """
    
    @rt("/")
    def index():
      data = Data("Start")
      textarea_elt = Textarea(name="textarea_name")
      label_elt = Label()
      mk.manage_binding(textarea_elt, Binding(data))
      mk.manage_binding(label_elt, Binding(data))
      return textarea_elt, label_elt
    
    user.open("/")
    user.should_see("Start")
    
    testable_textarea = user.find_element("textarea")
    testable_textarea.append(" + More")
    user.should_see("Start + More")
  
  def test_textarea_clear_works_with_binding(self, user, rt):
    """
    Clearing textarea should update binding to empty string.
    """
    
    @rt("/")
    def index():
      data = Data("Content to clear")
      textarea_elt = Textarea(name="textarea_name")
      label_elt = Label()
      mk.manage_binding(textarea_elt, Binding(data))
      mk.manage_binding(label_elt, Binding(data))
      return textarea_elt, label_elt
    
    user.open("/")
    user.should_see("Content to clear")
    
    testable_textarea = user.find_element("textarea")
    testable_textarea.clear()
    user.should_not_see("Content to clear")


class TestBindingSelect:
  """Tests for binding Select components (single selection)."""
  
  def test_i_can_bind_select(self):
    data = Data("")
    select_elt = Select(
      Option("Option 1", value="option1"),
      Option("Option 2", value="option2"),
      Option("Option 3", value="option3"),
      name="select_name"
    )
    
    binding = Binding(data)
    updated = mk.manage_binding(select_elt, binding)
    
    expected = Select(
      AttributeForbidden("hx_swap_oob"),
      Option("Option 1", value="option1"),
      Option("Option 2", value="option2"),
      Option("Option 3", value="option3"),
      name="select_name",
      id=AnyValue(),
      hx_post=f"{ROUTE_ROOT}{Routes.Bindings}",
    )
    assert matches(updated, expected)
  
  def test_i_can_update_select(self):
    data = Data("")
    select_elt = Select(
      Option("Option 1", value="option1"),
      Option("Option 2", value="option2"),
      Option("Option 3", value="option3"),
      name="select_name"
    )
    
    binding = Binding(data)
    mk.manage_binding(select_elt, binding)
    
    res = binding.update({"select_name": "option2"})
    
    expected = Select(
      Option("Option 1", value="option1"),
      Option("Option 2", value="option2", selected="true"),
      Option("Option 3", value="option3"),
      name="select_name",
      id=AnyValue(),
      hx_post=f"{ROUTE_ROOT}{Routes.Bindings}",
      hx_swap_oob="true"
    )
    assert matches(res, [expected])
  
  def test_i_can_change_selection(self):
    data = Data("")
    select_elt = Select(
      Option("Option 1", value="option1"),
      Option("Option 2", value="option2"),
      Option("Option 3", value="option3"),
      name="select_name"
    )
    
    binding = Binding(data)
    mk.manage_binding(select_elt, binding)
    
    binding.update({"select_name": "option2"})
    res = binding.update({"select_name": "option1"})
    
    expected = Select(
      Option("Option 1", value="option1", selected="true"),
      Option(AttributeForbidden("selected"), "Option 2", value="option2"),
      Option(AttributeForbidden("selected"), "Option 3", value="option3"),
      name="select_name",
      id=AnyValue(),
      hx_post=f"{ROUTE_ROOT}{Routes.Bindings}",
      hx_swap_oob="true"
    )
    assert matches(res, [expected])
  
  def test_i_can_bind_select_single(self, user, rt):
    """
    Single select should bind with data.
    Selecting an option should update the label.
    """
    
    @rt("/")
    def index():
      data = Data("option1")
      select_elt = Select(
        Option("Option 1", value="option1"),
        Option("Option 2", value="option2"),
        Option("Option 3", value="option3"),
        name="select_name"
      )
      label_elt = Label()
      mk.manage_binding(select_elt, Binding(data))
      mk.manage_binding(label_elt, Binding(data))
      return select_elt, label_elt
    
    user.open("/")
    user.should_see("option1")
    
    testable_select = user.find_element("select")
    testable_select.select("option2")
    user.should_see("option2")
    
    testable_select.select("option3")
    user.should_see("option3")
  
  def test_i_can_bind_select_by_text(self, user, rt):
    """
    Selecting by visible text should work with binding.
    """
    
    @rt("/")
    def index():
      data = Data("opt1")
      select_elt = Select(
        Option("First Option", value="opt1"),
        Option("Second Option", value="opt2"),
        Option("Third Option", value="opt3"),
        name="select_name"
      )
      label_elt = Label()
      mk.manage_binding(select_elt, Binding(data))
      mk.manage_binding(label_elt, Binding(data))
      return select_elt, label_elt
    
    user.open("/")
    user.should_see("opt1")
    
    testable_select = user.find_element("select")
    testable_select.select_by_text("Second Option")
    user.should_see("opt2")
  
  def test_select_with_default_selected_option(self, user, rt):
    """
    Select with a pre-selected option should initialize correctly.
    """
    
    @rt("/")
    def index():
      data = Data("option2")
      select_elt = Select(
        Option("Option 1", value="option1"),
        Option("Option 2", value="option2", selected=True),
        Option("Option 3", value="option3"),
        name="select_name"
      )
      label_elt = Label()
      mk.manage_binding(select_elt, Binding(data))
      mk.manage_binding(label_elt, Binding(data))
      return select_elt, label_elt
    
    user.open("/")
    user.should_see("option2")


class TestBindingSelectMultiple:
  """Tests for binding Select components with multiple selection."""
  
  def test_i_can_bind_select_multiple(self):
    data = Data("")
    select_elt = Select(
      Option("Option 1", value="option1"),
      Option("Option 2", value="option2"),
      Option("Option 3", value="option3"),
      name="select_name",
      multiple=True
    )
    
    binding = Binding(data)
    updated = mk.manage_binding(select_elt, binding)
    
    expected = Select(
      AttributeForbidden("hx_swap_oob"),
      Option("Option 1", value="option1"),
      Option("Option 2", value="option2"),
      Option("Option 3", value="option3"),
      name="select_name",
      id=AnyValue(),
      hx_post=f"{ROUTE_ROOT}{Routes.Bindings}",
    )
    assert matches(updated, expected)
  
  def test_i_can_update_one_selection(self):
    data = Data("")
    select_elt = Select(
      Option("Option 1", value="option1"),
      Option("Option 2", value="option2"),
      Option("Option 3", value="option3"),
      name="select_name",
      multiple=True
    )
    
    binding = Binding(data)
    mk.manage_binding(select_elt, binding)
    
    res = binding.update({"select_name": "option2"})
    
    expected = Select(
      Option("Option 1", value="option1"),
      Option("Option 2", value="option2", selected="true"),
      Option("Option 3", value="option3"),
      name="select_name",
      id=AnyValue(),
      hx_post=f"{ROUTE_ROOT}{Routes.Bindings}",
      hx_swap_oob="true"
    )
    assert matches(res, [expected])
  
  def test_i_can_update_multiple_selections(self):
    data = Data("")
    select_elt = Select(
      Option("Option 1", value="option1"),
      Option("Option 2", value="option2"),
      Option("Option 3", value="option3"),
      name="select_name",
      multiple=True
    )
    
    binding = Binding(data)
    mk.manage_binding(select_elt, binding)
    
    res = binding.update({"select_name": ["option2", "option3"]})
    
    expected = Select(
      Option(AttributeForbidden("selected"), "Option 1", value="option1"),
      Option("Option 2", value="option2", selected="true"),
      Option("Option 3", value="option3", selected="true"),
      name="select_name",
      id=AnyValue(),
      hx_post=f"{ROUTE_ROOT}{Routes.Bindings}",
      hx_swap_oob="true"
    )
    assert matches(res, [expected])
  
  def test_i_can_update_unselect(self):
    data = Data(["option1", "option2", "option3"])
    select_elt = Select(
      Option("Option 1", value="option1", selected="true"),
      Option("Option 2", value="option2", selected="true"),
      Option("Option 3", value="option3", selected="true"),
      name="select_name",
      multiple=True
    )
    
    binding = Binding(data)
    mk.manage_binding(select_elt, binding)
    
    res = binding.update({})
    
    expected = Select(
      Option(AttributeForbidden("selected"), "Option 1", value="option1"),
      Option(AttributeForbidden("selected"), "Option 2", value="option2"),
      Option(AttributeForbidden("selected"), "Option 3", value="option3"),
      name="select_name",
      id=AnyValue(),
      hx_post=f"{ROUTE_ROOT}{Routes.Bindings}",
      hx_swap_oob="true"
    )
    assert matches(res, [expected])
  
  def test_i_can_bind_select_multiple_with_label(self, user, rt):
    """
    Multiple select should bind with list data.
    Selecting multiple options should update the label.
    """
    
    @rt("/")
    def index():
      data = ListData(["option1"])
      select_elt = Select(
        Option("Option 1", value="option1"),
        Option("Option 2", value="option2"),
        Option("Option 3", value="option3"),
        name="select_name",
        multiple=True
      )
      label_elt = Label()
      mk.manage_binding(select_elt, Binding(data))
      mk.manage_binding(label_elt, Binding(data))
      return select_elt, label_elt
    
    user.open("/")
    user.should_see("['option1']")
    
    testable_select = user.find_element("select")
    testable_select.select("option2")
    user.should_see("['option1', 'option2']")
    
    testable_select.select("option3")
    user.should_see("['option1', 'option2', 'option3']")
  
  def test_i_can_deselect_from_multiple_select(self, user, rt):
    """
    Deselecting options from multiple select should update binding.
    """
    
    @rt("/")
    def index():
      data = ListData(["option1", "option2"])
      select_elt = Select(
        Option("Option 1", value="option1"),
        Option("Option 2", value="option2"),
        Option("Option 3", value="option3"),
        name="select_name",
        multiple=True
      )
      label_elt = Label()
      mk.manage_binding(select_elt, Binding(data))
      mk.manage_binding(label_elt, Binding(data))
      return select_elt, label_elt
    
    user.open("/")
    user.should_see("['option1', 'option2']")
    
    testable_select = user.find_element("select")
    testable_select.deselect("option1")
    user.should_see("option2")


class TestBindingRange:
  """Tests for binding Range (slider) components."""
  
  def test_i_can_bind_range(self):
    data = Data(50)
    range_elt = Input(
      type="range",
      name="range_name",
      min="0",
      max="100",
      value="50"
    )
    
    binding = Binding(data)
    updated = mk.manage_binding(range_elt, binding)
    
    expected = Input(
      AttributeForbidden("hx_swap_oob"),
      type="range",
      name="range_name",
      min="0",
      max="100",
      value=50,
      hx_post=f"{ROUTE_ROOT}{Routes.Bindings}",
      id=AnyValue(),
    )
    assert matches(updated, expected)
  
  def test_i_can_update_range(self):
    data = Data(50)
    range_elt = Input(
      type="range",
      name="range_name",
      min="0",
      max="100",
      value="50"
    )
    
    binding = Binding(data)
    mk.manage_binding(range_elt, binding)
    
    res = binding.update({"range_name": 25})
    
    expected = [Input(
      type="range",
      name="range_name",
      min="0",
      max="100",
      value=25,
      hx_post=f"{ROUTE_ROOT}{Routes.Bindings}",
      id=AnyValue(),
      hx_swap_oob="true"
    )]
    assert matches(res, expected)
  
  def test_i_can_bind_range_with_label(self, user, rt):
    """
    Range input should bind with numeric data.
    Changing the slider should update the label.
    """
    
    @rt("/")
    def index():
      data = NumericData(50)
      range_elt = Input(
        type="range",
        name="range_name",
        min="0",
        max="100",
        value="50"
      )
      label_elt = Label()
      mk.manage_binding(range_elt, Binding(data))
      mk.manage_binding(label_elt, Binding(data))
      return range_elt, label_elt
    
    user.open("/")
    user.should_see("50")
    
    testable_range = user.find_element("input[type='range']")
    testable_range.set(75)
    user.should_see("75")
    
    testable_range.set(25)
    user.should_see("25")
  
  def test_range_increase_decrease(self, user, rt):
    """
    Increasing and decreasing range should update binding.
    """
    
    @rt("/")
    def index():
      data = NumericData(50)
      range_elt = Input(
        type="range",
        name="range_name",
        min="0",
        max="100",
        step="10",
        value="50"
      )
      label_elt = Label()
      mk.manage_binding(range_elt, Binding(data))
      mk.manage_binding(label_elt, Binding(data))
      return range_elt, label_elt
    
    user.open("/")
    user.should_see("50")
    
    testable_range = user.find_element("input[type='range']")
    testable_range.increase()
    user.should_see("60")
    
    testable_range.increase()
    user.should_see("70")
    
    testable_range.decrease()
    user.should_see("60")
  
  def test_range_clamping_to_min_max(self, user, rt):
    """
    Range values should be clamped to min/max bounds.
    """
    
    @rt("/")
    def index():
      data = NumericData(50)
      range_elt = Input(
        type="range",
        name="range_name",
        min="0",
        max="100",
        value="50"
      )
      label_elt = Label()
      mk.manage_binding(range_elt, Binding(data))
      mk.manage_binding(label_elt, Binding(data))
      return range_elt, label_elt
    
    user.open("/")
    
    testable_range = user.find_element("input[type='range']")
    testable_range.set(150)  # Above max
    user.should_see("100")
    
    testable_range.set(-10)  # Below min
    user.should_see("0")


class TestBindingRadio:
  """Tests for binding Radio button components."""
  
  def test_i_can_bind_radio_buttons(self):
    data = Data()
    radio1 = Input(type="radio", name="radio_name", value="option1")
    radio2 = Input(type="radio", name="radio_name", value="option2")
    radio3 = Input(type="radio", name="radio_name", value="option3")
    
    binding = Binding(data)
    mk.manage_binding(radio1, binding)
    mk.manage_binding(radio2, Binding(data))
    mk.manage_binding(radio3, Binding(data))
    
    res = binding.update({"radio_name": "option1"})  # option1 is selected
    expected = [
        Input(type="radio", name="radio_name", value="option1", checked="true", hx_swap_oob="true"),
        Input(AttributeForbidden("checked"), type="radio", name="radio_name", value="option2", hx_swap_oob="true"),
        Input(AttributeForbidden("checked"), type="radio", name="radio_name", value="option3", hx_swap_oob="true"),
    ]
    assert matches(res, expected)
  
  def test_i_can_bind_radio_buttons_and_label(self, user, rt):
    """
    Radio buttons should bind with data.
    Selecting a radio should update the label.
    """
    
    @rt("/")
    def index():
      data = Data()
      radio1 = Input(type="radio", name="radio_name", value="option1", checked="true")
      radio2 = Input(type="radio", name="radio_name", value="option2")
      radio3 = Input(type="radio", name="radio_name", value="option3")
      label_elt = Label()
      
      mk.manage_binding(radio1, Binding(data))
      mk.manage_binding(radio2, Binding(data))
      mk.manage_binding(radio3, Binding(data))
      mk.manage_binding(label_elt, Binding(data))
      
      return radio1, radio2, radio3, label_elt
    
    user.open("/")
    
    # Select second radio
    testable_radio2 = user.find_element("input[value='option2']")
    testable_radio2.select()
    user.should_see("option2")
    
    # Select third radio
    testable_radio3 = user.find_element("input[value='option3']")
    testable_radio3.select()
    user.should_see("option3")
  
  def test_radio_initial_state(self, user, rt):
    """
    Radio buttons should initialize with correct checked state.
    """
    
    @rt("/")
    def index():
      data = Data("option2")
      radio1 = Input(type="radio", name="radio_name", value="option1")
      radio2 = Input(type="radio", name="radio_name", value="option2", checked=True)
      radio3 = Input(type="radio", name="radio_name", value="option3")
      label_elt = Label()
      
      mk.manage_binding(radio1, Binding(data))
      mk.manage_binding(radio2, Binding(data))
      mk.manage_binding(radio3, Binding(data))
      mk.manage_binding(label_elt, Binding(data))
      
      return radio1, radio2, radio3, label_elt
    
    user.open("/")
    user.should_see("option2")


class TestBindingDatalist:
  """Tests for binding Input with Datalist (combobox)."""
  
  def test_i_can_bind_datalist(self):
    data = Data(["suggestion2"])
    datalist = Datalist(
      Option(value="suggestion1"),
      id="suggestions"
    )
    
    updated = mk.manage_binding(datalist, Binding(data))
    expected = Datalist(
      Option(value="suggestion2"),
      id="suggestions"
    )
    
    assert matches(updated, expected)


class TestBindingEdgeCases:
  """Tests for edge cases and special scenarios."""
  
  def test_multiple_components_bind_to_same_data(self, user, rt):
    """
    Multiple different components can bind to the same data object.
    """
    
    @rt("/")
    def index():
      data = Data("synchronized")
      
      input_elt = Input(name="input_name")
      textarea_elt = Textarea(name="textarea_name")
      label_elt = Label()
      
      mk.manage_binding(input_elt, Binding(data))
      mk.manage_binding(textarea_elt, Binding(data))
      mk.manage_binding(label_elt, Binding(data))
      
      return input_elt, textarea_elt, label_elt
    
    user.open("/")
    user.should_see("synchronized")
    
    # Change via input
    testable_input = user.find_element("input")
    testable_input.send("changed via input")
    user.should_see("changed via input")
    
    # Change via textarea
    testable_textarea = user.find_element("textarea")
    testable_textarea.send("changed via textarea")
    user.should_see("changed via textarea")
  
  def test_component_without_name_attribute(self, user, rt):
    """
    Component without name attribute should handle gracefully.
    """
    
    @rt("/")
    def index():
      data = Data("test")
      # Input without name - should not crash
      input_elt = Input()  # No name attribute
      label_elt = Label()
      
      mk.manage_binding(label_elt, Binding(data))
      
      return input_elt, label_elt
    
    user.open("/")
    user.should_see("test")
  
  def test_binding_with_initial_empty_string(self, user, rt):
    """
    Binding should work correctly with empty string initial values.
    """
    
    @rt("/")
    def index():
      data = Data("")
      input_elt = Input(name="input_name")
      label_elt = Label()
      
      mk.manage_binding(input_elt, Binding(data))
      mk.manage_binding(label_elt, Binding(data))
      
      return input_elt, label_elt
    
    user.open("/")
    
    testable_input = user.find_element("input")
    testable_input.send("now has value")
    user.should_see("now has value")
  
  def test_binding_with_special_characters(self, user, rt):
    """
    Binding should handle special characters correctly.
    """
    
    @rt("/")
    def index():
      data = Data("Hello")
      input_elt = Input(name="input_name")
      label_elt = Label()
      
      mk.manage_binding(input_elt, Binding(data))
      mk.manage_binding(label_elt, Binding(data))
      
      return input_elt, label_elt
    
    user.open("/")
    
    testable_input = user.find_element("input")
    testable_input.send("Special: <>&\"'")
    user.should_see("Special: <>&\"'")


class TestCheckBox:
  def test_i_can_bind_checkbox(self):
    data = Data("")
    check_box = Input(name="checkbox_name", type="checkbox")
    
    binding = Binding(data)
    mk.manage_binding(check_box, binding)
    
    # checkbox is selected
    res = binding.update({"checkbox_name": "on"})
    expected = [Input(name="checkbox_name", type="checkbox", checked="true", hx_swap_oob="true")]
    assert matches(res, expected)
    
    # check box is not selected
    res = binding.update({})
    expected = [Input(AttributeForbidden("checked"), name="checkbox_name", type="checkbox", hx_swap_oob="true")]
    assert matches(res, expected)
  
  def test_checkbox_initial_state_false(self):
    data = Data(False)
    check_box = Input(name="checkbox_name", type="checkbox")
    
    binding = Binding(data)
    updated = mk.manage_binding(check_box, binding)
    
    expected = Input(AttributeForbidden("checked"), name="checkbox_name", type="checkbox")
    assert matches(updated, expected)
  
  def test_checkbox_initial_state_true(self):
    data = Data(True)
    check_box = Input(name="checkbox_name", type="checkbox")
    
    binding = Binding(data)
    updated = mk.manage_binding(check_box, binding)
    
    expected = Input(name="checkbox_name", type="checkbox", checked="true")
    assert matches(updated, expected)
  
  def test_i_can_bind_checkbox_and_label_without_converter(self, user, rt):
    @rt("/")
    def index():
      data = Data(True)
      input_elt = Input(name="input_name", type="checkbox")
      label_elt = Label()
      mk.manage_binding(input_elt, Binding(data))
      mk.manage_binding(label_elt, Binding(data))
      return input_elt, label_elt
    
    user.open("/")
    user.should_see("True")
    testable_input = user.find_element("input")
    
    testable_input.check()
    user.should_see("on")
    
    testable_input.uncheck()
    user.should_not_see("on")
  
  def test_i_can_bind_checkbox_and_label_with_converter(self, user, rt):
    @rt("/")
    def index():
      data = Data(True)
      input_elt = Input(name="input_name", type="checkbox")
      label_elt = Label()
      mk.manage_binding(input_elt, Binding(data))
      mk.manage_binding(label_elt, Binding(data, converter=BooleanConverter()))
      return input_elt, label_elt
    
    user.open("/")
    user.should_see("True")
    testable_input = user.find_element("input")
    
    testable_input.check()
    user.should_see("True")
    
    testable_input.uncheck()
    user.should_see("False")
