import pytest
from fasthtml.components import Div
from fasthtml.fastapp import fast_app

from myfasthtml.test.testclient import MyTestClient, TestableElement, TestableForm


class TestMyTestClientOpen:
  
  def test_i_can_open_a_page(self):
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get(): return "hello world"
    
    client.open("/")
    
    assert client.get_content() == "hello world"
  
  def test_i_can_open_a_page_when_html(self):
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get(): return Div("hello world")
    
    client.open("/")
    
    assert client.get_content() == ' <!doctype html>\n <html>\n   <head>\n     <title>FastHTML page</title>\n     <link rel="canonical" href="http://testserver/">\n   </head>\n   <body>\n     <div>hello world</div>\n   </body>\n </html>\n'
  
  def test_i_cannot_open_a_page_not_defined(self):
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    with pytest.raises(AssertionError) as exc_info:
      client.open("/not_found")
    
    assert str(exc_info.value) == "Failed to open '/not_found'. status code=404 : reason='404 Not Found'"


class TestMyTestClientShouldSee:
  def test_i_can_see_text_in_plain_response(self):
    """Test that should_see() works with plain text responses."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return "hello world"
    
    client.open("/").should_see("hello world")
  
  def test_i_can_see_text_in_html_response(self):
    """Test that should_see() extracts visible text from HTML responses."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return "<html><body><h1>Welcome</h1><p>This is a test</p></body></html>"
    
    client.open("/").should_see("Welcome").should_see("This is a test")
  
  def test_i_can_see_text_ignoring_html_tags(self):
    """Test that should_see() searches in visible text only, not in HTML tags."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return '<div class="container">Content</div>'
    
    # Should find the visible text
    client.open("/").should_see("Content")
    
    # Should NOT find text that's only in attributes/tags
    with pytest.raises(AssertionError) as exc_info:
      client.should_see("container")
    
    assert "Expected to see 'container' in page content but it was not found" in str(exc_info.value)
  
  def test_i_cannot_see_text_that_is_not_present(self):
    """Test that should_see() raises AssertionError when text is not found."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return "hello world"
    
    with pytest.raises(AssertionError) as exc_info:
      client.open("/").should_see("goodbye")
    
    assert "Expected to see 'goodbye' in page content but it was not found" in str(exc_info.value)
    assert "hello world" in str(exc_info.value)
  
  def test_i_cannot_call_should_see_without_opening_page(self):
    """Test that should_see() raises ValueError if no page has been opened."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    with pytest.raises(ValueError) as exc_info:
      client.should_see("anything")
    
    assert str(exc_info.value) == "No page content available. Call open() before should_see()."
  
  def test_i_can_verify_text_is_not_present(self):
    """Test that should_not_see() works when text is absent."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return "hello world"
    
    client.open("/").should_not_see("goodbye")
  
  def test_i_cannot_use_should_not_see_when_text_is_present(self):
    """Test that should_not_see() raises AssertionError when text is found."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return "hello world"
    
    with pytest.raises(AssertionError) as exc_info:
      client.open("/").should_not_see("hello")
    
    error_message = str(exc_info.value)
    assert "Expected NOT to see 'hello' in page content but it was found" in error_message
  
  def test_i_cannot_call_should_not_see_without_opening_page(self):
    """Test that should_not_see() raises ValueError if no page has been opened."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    with pytest.raises(ValueError) as exc_info:
      client.should_not_see("anything")
    
    assert str(exc_info.value) == "No page content available. Call open() before should_not_see()."
  
  def test_i_can_chain_multiple_assertions(self):
    """Test that assertions can be chained together."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return "<html><body><h1>Welcome</h1><p>Content here</p></body></html>"
    
    # Chain multiple assertions
    client.open("/").should_see("Welcome").should_see("Content").should_not_see("Error")
  
  def test_i_can_see_element_context_when_text_should_not_be_seen(self):
    """Test that the HTML element containing the text is displayed with parent context."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return '<div class="container"><p class="content">forbidden text</p></div>'
    
    with pytest.raises(AssertionError) as exc_info:
      client.open("/").should_not_see("forbidden text")
    
    error_message = str(exc_info.value)
    assert "Found in:" in error_message
    assert '<p class="content">forbidden text</p>' in error_message
    assert '<div class="container">' in error_message
  
  def test_i_can_configure_parent_levels_in_constructor(self):
    """Test that parent_levels parameter controls the number of parent levels shown."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app, parent_levels=2)
    
    @rt('/')
    def get():
      return '<body><div class="wrapper"><div class="container"><p>error</p></div></div></body>'
    
    with pytest.raises(AssertionError) as exc_info:
      client.open("/").should_not_see("error")
    
    error_message = str(exc_info.value)
    assert '<p>error</p>' in error_message
    assert '<div class="container">' in error_message
    assert '<div class="wrapper">' in error_message
  
  def test_i_can_find_text_in_nested_elements(self):
    """Test that the smallest element containing the text is found in nested structures."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return '<div><section><article><p class="target">nested text</p></article></section></div>'
    
    with pytest.raises(AssertionError) as exc_info:
      client.open("/").should_not_see("nested text")
    
    error_message = str(exc_info.value)
    # Should find the <p> element, not the outer <div>
    assert '<p class="target">nested text</p>' in error_message
  
  def test_i_can_find_fragmented_text_across_tags(self):
    """Test that text fragmented across multiple tags is correctly found."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return '<p class="message">hel<span>lo</span> world</p>'
    
    with pytest.raises(AssertionError) as exc_info:
      client.open("/").should_not_see("hello world")
    
    error_message = str(exc_info.value)
    # Should find the parent <p> element that contains the full text
    assert '<p class="message">' in error_message
  
  def test_i_do_not_find_text_in_html_attributes(self):
    """Test that text in HTML attributes is not considered as visible text."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return '<div class="error message" title="error info">Success</div>'
    
    # "error" is in attributes but not in visible text
    client.open("/").should_not_see("error")
    
    # "Success" is in visible text
    with pytest.raises(AssertionError):
      client.should_not_see("Success")


class TestMyTestClientFindElement:
  
  @pytest.mark.parametrize("selector,expected_tag", [
      ("#unique-id", '<div class="main wrapper"'),
      (".home-link", '<a class="link home-link"'),
      ("div > div", '<div class="content"'),
      ("div span", "<span"),
      ("[data-type]", '<a class="link home-link"'),
      ('[data-type="navigation"]', '<a class="link home-link"'),
      ('[class~="link"]', '<a class="link home-link"'),
      ('[href^="/home"]', '<a class="link home-link"'),
      ('[href$="about"]', '<a href="/about">'),
      ('[data-author*="john"]', '<a class="link home-link"'),
  ])
  def test_i_can_find_element(self, selector, expected_tag):
    """Test that find_element works with various CSS selectors."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return '''
          <div id="unique-id" class="main wrapper">
            <a href="/home" class="link home-link" data-type="navigation" data-author="john-doe">Home</a>
            <a href="/about">About</a>
            <div class="content">
              <span class="text">Content</span>
            </div>
          </div>
          '''
    
    element = client.open("/").find_element(selector)
    
    assert element is not None
    assert isinstance(element, TestableElement)
    assert expected_tag in element.html_fragment
  
  def test_i_cannot_find_element_when_none_exists(self):
    """Test that find_element raises AssertionError when no element matches."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return '<div class="container"><p>Content</p></div>'
    
    with pytest.raises(AssertionError) as exc_info:
      client.open("/").find_element("#non-existent")
    
    assert "No element found matching selector '#non-existent'" in str(exc_info.value)
  
  def test_i_cannot_find_element_when_multiple_exist(self):
    """Test that find_element raises AssertionError when multiple elements match."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return '<div><p class="text">First</p><p class="text">Second</p><p class="text">Third</p></div>'
    
    with pytest.raises(AssertionError) as exc_info:
      client.open("/").find_element(".text")
    
    error_message = str(exc_info.value)
    assert "Found 3 elements matching selector '.text'" in error_message
    assert "Expected exactly 1" in error_message
  
  def test_i_cannot_call_find_element_without_opening_page(self):
    """Test that find_element raises ValueError if no page has been opened."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    with pytest.raises(ValueError) as exc_info:
      client.find_element("#any-selector")
    
    assert str(exc_info.value) == "No page content available. Call open() before find_element()."


class TestMyTestClientFindForm:
  def test_i_can_find_form(self):
    """Test that find_form works fo simple form."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return '''<form></form>'''
    
    form = client.open("/").find_form()
    
    assert form is not None
    assert isinstance(form, TestableForm)
  
  def test_i_can_find_form_in_nested_elements(self):
    """Test that find_form works when form is nested in other elements."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return '''
          <div class="container">
            <div class="wrapper">
              <form></form>
            </div>
          </div>
      '''
    
    form = client.open("/").find_form()
    
    assert form is not None
    assert isinstance(form, TestableForm)
  
  def test_i_can_find_form_with_all_fields(self):
    """Test that find_form works when form has fields."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return '''
          <form>
            <input type="text" name="username" placeholder="Username">
            <input type="password" name="password" placeholder="Password">
            <button type="submit">Sign In</button>
          </form>
      '''
    
    form = client.open("/").find_form(fields=["username", "password"])
    
    assert form is not None
    assert isinstance(form, TestableForm)
  
  def test_i_can_find_form_with_label(self):
    """Test that find_form works when form has fields."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return '''
          <form>
            <label for="username">Username</label>
            <input type="text" name="username" placeholder="Username">
            <input type="password" name="password" placeholder="Password">
            <button type="submit">Sign In</button>
          </form>
      '''
    
    form = client.open("/").find_form(fields=["Username", "password"])
    
    assert form is not None
    assert isinstance(form, TestableForm)
  
  def test_i_can_find_form_with_one_field(self):
    """Test that find_form works when form has at least one field."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return '''
          <form>
            <input type="text" name="username" placeholder="Username">
            <input type="password" name="password" placeholder="Password">
            <button type="submit">Sign In</button>
          </form>
      '''
    
    form = client.open("/").find_form(fields=["username"])
    
    assert form is not None
    assert isinstance(form, TestableForm)
  
  def test_i_cannot_find_element_when_none_exists(self):
    """Test that find_form raises AssertionError when no form found."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return '<div class="container"><p>Content</p></div>'
    
    with pytest.raises(AssertionError) as exc_info:
      client.open("/").find_form()
    
    assert "No form found" in str(exc_info.value)
  
  def test_i_cannot_call_find_form_without_opening_page(self):
    """Test that find_form raises ValueError if no page has been opened."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    with pytest.raises(ValueError) as exc_info:
      client.find_form()
    
    assert str(exc_info.value) == "No page content available. Call open() before find_form()."
  
  def test_cannot_find_form_when_multiple_exist(self):
    """Test that find_form raises AssertionError when multiple forms match."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return '''
          <div class="container">
            <form></form>
            <form></form>
          </div>
      '''
    
    with pytest.raises(AssertionError) as exc_info:
      client.open("/").find_form()
    
    error_message = str(exc_info.value)
    assert "Found 2 forms (with the specified fields). Expected exactly 1." in error_message
  
  def test_cannot_find_form_with_fields_when_multiple_exist(self):
    """Test that find_form raises AssertionError when multiple forms match."""
    test_app, rt = fast_app(default_hdrs=False)
    client = MyTestClient(test_app)
    
    @rt('/')
    def get():
      return '''
          <div class="container">
            <form>
              <input type="text" name="username" placeholder="Username">
              <input type="password" name="password" placeholder="Password">
              <button type="submit">Sign In</button>
            </form>
            <form>
              <input type="text" name="username" placeholder="Username">
              <input type="password" name="password" placeholder="Password">
              <button type="submit">Sign In</button>
            </form>
          </div>
      '''
    
    with pytest.raises(AssertionError) as exc_info:
      client.open("/").find_form(fields=["username", "password"])
    
    error_message = str(exc_info.value)
    assert "Found 2 forms (with the specified fields). Expected exactly 1." in error_message
