import pytest
from fastcore.basics import NotStr
from fasthtml.components import *

from myfasthtml.test.matcher import matches, StartsWith, Contains, DoesNotContain, Empty, DoNotCheck, ErrorOutput, \
  ErrorComparisonOutput, AttributeForbidden, AnyValue, NoChildren
from myfasthtml.test.testclient import MyFT


@pytest.mark.parametrize('actual, expected', [
    (None, None),
    (123, 123),
    (Div(), Div()),
    ([Div(), Span()], [Div(), Span()]),
    (Div(attr1="value"), Div(attr1="value")),
    (Div(attr1="value", attr2="value"), Div(attr1="value")),
    (Div(attr1="valueXXX", attr2="value"), Div(attr1=StartsWith("value"))),
    (Div(attr1="before value after", attr2="value"), Div(attr1=Contains("value"))),
    (Div(attr1="before after", attr2="value"), Div(attr1=DoesNotContain("value"))),
    (Div(attr1="value"), Div(attr1=AnyValue())),
    (None, DoNotCheck()),
    (123, DoNotCheck()),
    (Div(), DoNotCheck()),
    ([Div(), Span()], DoNotCheck()),
    (NotStr("123456"), NotStr("123")),  # for NotStr, only the beginning is checked
    (Div(), Div(Empty())),
    (Div(), Div(NoChildren())),
    (Div(attr1="value"), Div(NoChildren())),
    (Div(attr1="value1"), Div(AttributeForbidden("attr2"))),
    (Div(123), Div(123)),
    (Div(Span(123)), Div(Span(123))),
    (Div(Span(123)), Div(DoNotCheck())),
])
def test_i_can_match(actual, expected):
  assert matches(actual, expected)


@pytest.mark.parametrize('actual, expected, error_message', [
    (None, Div(), "Actual is None"),
    (Div(), None, "Actual is not None"),
    (123, Div(), "The types are different"),
    (123, 124, "The values are different"),
    ([Div(), Span()], [], "Actual is bigger than expected"),
    ([], [Div(), Span()], "Actual is smaller than expected"),
    ("not a list", [Div(), Span()], "The types are different"),
    ([Div(), Span()], [Div(), 123], "The types are different"),
    (Div(), Span(), "The elements are different"),
    ([Div(), Span()], [Div(), Div()], "The elements are different"),
    (Div(), Div(attr1="value"), "'attr1' is not found in Actual"),
    (Div(attr2="value"), Div(attr1="value"), "'attr1' is not found in Actual"),
    (Div(attr1="value1"), Div(attr1="value2"), "The values are different for 'attr1'"),
    (Div(attr1="value1"), Div(attr1=StartsWith("value2")), "The condition 'StartsWith(value2)' is not satisfied"),
    (Div(attr1="value1"), Div(attr1=Contains("value2")), "The condition 'Contains(value2)' is not satisfied"),
    (Div(attr1="value1 value2"), Div(attr1=DoesNotContain("value2")), "The condition 'DoesNotContain(value2)'"),
    (Div(attr1=None), Div(attr1=AnyValue()), "'attr1' is not found in Actual"),
    (Div(), Div(attr1=AnyValue()), "'attr1' is not found in Actual"),
    (NotStr("456"), NotStr("123"), "Notstr values are different"),
    (Div(attr="value"), Div(Empty()), "The condition 'Empty()' is not satisfied"),
    (Div(Span()), Div(NoChildren()), "The condition 'NoChildren()' is not satisfied"),
    (Div(120), Div(Empty()), "The condition 'Empty()' is not satisfied"),
    (Div(Span()), Div(Empty()), "The condition 'Empty()' is not satisfied"),
    (Div(), Div(Span()), "Actual is lesser than expected"),
    (Div(), Div(123), "Actual is lesser than expected"),
    (Div(Span()), Div(Div()), "The elements are different"),
    (Div(123), Div(Div()), "The types are different"),
    (Div(123), Div(456), "The values are different"),
    (Div(Span(), Span()), Div(Span(), Div()), "The elements are different"),
    (Div(Span(Div())), Div(Span(Span())), "The elements are different"),
    (Div(attr1="value1"), Div(AttributeForbidden("attr1")), "condition 'AttributeForbidden(attr1)' is not satisfied"),
])
def test_i_can_detect_errors(actual, expected, error_message):
  with pytest.raises(AssertionError) as exc_info:
    matches(actual, expected)
  assert error_message in str(exc_info.value)


@pytest.mark.parametrize('element, expected_path', [
    (Div(), "Path  : 'div"),
    (Div(Span()), "Path  : 'div.span"),
    (Div(Span(Div())), "Path  : 'div.span.div"),
    (Div(id="div_id"), "Path  : 'div#div_id"),
    (Div(cls="div_class"), "Path  : 'div[class=div_class]"),
    (Div(name="div_class"), "Path  : 'div[name=div_class]"),
    (Div(attr="value"), "Path  : 'div"),
    (Div(Span(Div(), cls="span_class"), id="div_id"), "Path  : 'div#div_id.span[class=span_class].div"),
])
def test_i_can_properly_show_path(element, expected_path):
  def _construct_test_element(source, tail):
    res = MyFT(source.tag, source.attrs)
    if source.children:
      res.children = [_construct_test_element(child, tail) for child in source.children]
    else:
      res.children = [tail]
    return res
  
  with pytest.raises(AssertionError) as exc_info:
    actual = _construct_test_element(element, "Actual")
    expected = _construct_test_element(element, "Expected")
    matches(actual, expected)
  
  assert expected_path in str(exc_info.value)


def test_i_can_output_error_path():
  elt = Div()
  expected = Div()
  path = "div#div_id.div.span[class=span_class].p[name=p_name].div"
  error_output = ErrorOutput(path, elt, expected)
  error_output.compute()
  assert error_output.output == ['(div "id"="div_id" ...',
                                 '  (div ...',
                                 '    (span "class"="span_class" ...',
                                 '      (p "name"="p_name" ...',
                                 '        (div )']


def test_i_can_output_error_attribute():
  elt = Div(attr1="value1", attr2="value2")
  expected = elt
  path = ""
  error_output = ErrorOutput(path, elt, expected)
  error_output.compute()
  assert error_output.output == ['(div "attr1"="value1" "attr2"="value2")']


def test_i_can_output_error_attribute_missing_1():
  elt = Div(attr2="value2")
  expected = Div(attr1="value1", attr2="value2")
  path = ""
  error_output = ErrorOutput(path, elt, expected)
  error_output.compute()
  assert error_output.output == ['(div "attr1"="** MISSING **" "attr2"="value2")',
                                 '     ^^^^^^^^^^^^^^^^^^^^^^^                 ']


def test_i_can_output_error_attribute_missing_2():
  elt = Div(attr1="value1")
  expected = Div(attr1="value1", attr2="value2")
  path = ""
  error_output = ErrorOutput(path, elt, expected)
  error_output.compute()
  assert error_output.output == ['(div "attr1"="value1" "attr2"="** MISSING **")',
                                 '                      ^^^^^^^^^^^^^^^^^^^^^^^']


def test_i_can_output_error_attribute_wrong_value():
  elt = Div(attr1="value3", attr2="value2")
  expected = Div(attr1="value1", attr2="value2")
  path = ""
  error_output = ErrorOutput(path, elt, expected)
  error_output.compute()
  assert error_output.output == ['(div "attr1"="value3" "attr2"="value2")',
                                 '     ^^^^^^^^^^^^^^^^                 ']


def test_i_can_output_error_constant():
  elt = 123
  expected = elt
  path = ""
  error_output = ErrorOutput(path, elt, expected)
  error_output.compute()
  assert error_output.output == ['123']


def test_i_can_output_error_constant_wrong_value():
  elt = 123
  expected = 456
  path = ""
  error_output = ErrorOutput(path, elt, expected)
  error_output.compute()
  assert error_output.output == ['123',
                                 '^^^']


def test_i_can_output_error_when_predicate():
  elt = "before value after"
  expected = Contains("value")
  path = ""
  error_output = ErrorOutput(path, elt, expected)
  error_output.compute()
  assert error_output.output == ["before value after"]


def test_i_can_output_error_when_predicate_wrong_value():
  """I can display error when the condition predicate is not satisfied."""
  elt = "before after"
  expected = Contains("value")
  path = ""
  error_output = ErrorOutput(path, elt, expected)
  error_output.compute()
  assert error_output.output == ["before after",
                                 "^^^^^^^^^^^^"]


def test_i_can_output_error_child_element():
  """I can display error when the element has children"""
  elt = Div(P(id="p_id"), Div(id="child_1"), Div(id="child_2"), attr1="value1")
  expected = elt
  path = ""
  error_output = ErrorOutput(path, elt, expected)
  error_output.compute()
  assert error_output.output == ['(div "attr1"="value1"',
                                 '  (p "id"="p_id")',
                                 '  (div "id"="child_1")',
                                 '  (div "id"="child_2")',
                                 ')',
                                 ]


def test_i_can_output_error_child_element_text():
  """I can display error when the children is not a FT"""
  elt = Div("Hello world", Div(id="child_1"), Div(id="child_2"), attr1="value1")
  expected = elt
  path = ""
  error_output = ErrorOutput(path, elt, expected)
  error_output.compute()
  assert error_output.output == ['(div "attr1"="value1"',
                                 '  "Hello world"',
                                 '  (div "id"="child_1")',
                                 '  (div "id"="child_2")',
                                 ')',
                                 ]


def test_i_can_output_error_child_element_indicating_sub_children():
  elt = Div(P(id="p_id"), Div(Div(id="child_2"), id="child_1"), attr1="value1")
  expected = elt
  path = ""
  error_output = ErrorOutput(path, elt, expected)
  error_output.compute()
  assert error_output.output == ['(div "attr1"="value1"',
                                 '  (p "id"="p_id")',
                                 '  (div "id"="child_1" ...)',
                                 ')',
                                 ]


def test_i_can_output_error_child_element_wrong_value():
  elt = Div(P(id="p_id"), Div(id="child_2"), attr1="value1")
  expected = Div(P(id="p_id"), Div(id="child_1"), attr1="value1")
  path = ""
  error_output = ErrorOutput(path, elt, expected)
  error_output.compute()
  assert error_output.output == ['(div "attr1"="value1"',
                                 '  (p "id"="p_id")',
                                 '  (div "id"="child_2")',
                                 '       ^^^^^^^^^^^^^^',
                                 ')',
                                 ]


def test_i_can_output_error_fewer_elements():
  elt = Div(P(id="p_id"), attr1="value1")
  expected = Div(P(id="p_id"), Div(id="child_1"), attr1="value1")
  path = ""
  error_output = ErrorOutput(path, elt, expected)
  error_output.compute()
  assert error_output.output == ['(div "attr1"="value1"',
                                 '  (p "id"="p_id")',
                                 '  ! ** MISSING ** !',
                                 ')',
                                 ]


def test_i_can_output_comparison():
  actual = Div(P(id="p_id"), attr1="value1")
  expected = actual
  actual_out = ErrorOutput("", actual, expected)
  expected_out = ErrorOutput("", expected, expected)
  
  comparison_out = ErrorComparisonOutput(actual_out, expected_out)
  
  res = comparison_out.render()
  
  assert "\n" + res == '''
(div "attr1"="value1" | (div "attr1"="value1"
  (p "id"="p_id")     |   (p "id"="p_id")
)                     | )'''


def test_i_can_output_comparison_with_path():
  actual = Div(P(id="p_id"), attr1="value1")
  expected = actual
  actual_out = ErrorOutput("div#div_id.span[class=cls].div", actual, expected)
  expected_out = ErrorOutput("div#div_id.span[class=cls].div", expected, expected)
  
  comparison_out = ErrorComparisonOutput(actual_out, expected_out)
  
  res = comparison_out.render()
  
  assert "\n" + res == '''
(div "id"="div_id" ...    | (div "id"="div_id" ...
  (span "class"="cls" ... |   (span "class"="cls" ...
    (div "attr1"="value1" |     (div "attr1"="value1"
      (p "id"="p_id")     |       (p "id"="p_id")
    )                     |     )'''


def test_i_can_output_comparison_when_missing_attributes():
  actual = Div(P(id="p_id"), attr1="value1")
  expected = Div(P(id="p_id"), attr2="value1")
  actual_out = ErrorOutput("", actual, expected)
  expected_out = ErrorOutput("", expected, expected)
  
  comparison_out = ErrorComparisonOutput(actual_out, expected_out)
  
  res = comparison_out.render()
  
  assert "\n" + res == '''
(div "attr2"="** MISSING **" | (div "attr2"="value1"
     ^^^^^^^^^^^^^^^^^^^^^^^ |
  (p "id"="p_id")            |   (p "id"="p_id")
)                            | )'''


def test_i_can_output_comparison_when_wrong_attributes():
  actual = Div(P(id="p_id"), attr1="value2")
  expected = Div(P(id="p_id"), attr1="value1")
  actual_out = ErrorOutput("", actual, expected)
  expected_out = ErrorOutput("", expected, expected)
  
  comparison_out = ErrorComparisonOutput(actual_out, expected_out)
  
  res = comparison_out.render()
  
  assert "\n" + res == '''
(div "attr1"="value2" | (div "attr1"="value1"
     ^^^^^^^^^^^^^^^^ |
  (p "id"="p_id")     |   (p "id"="p_id")
)                     | )'''


def test_i_can_output_comparison_when_fewer_elements():
  actual = Div(P(id="p_id"), attr1="value1")
  expected = Div(Span(id="s_id"), P(id="p_id"), attr1="value1")
  actual_out = ErrorOutput("", actual, expected)
  expected_out = ErrorOutput("", expected, expected)
  
  comparison_out = ErrorComparisonOutput(actual_out, expected_out)
  
  res = comparison_out.render()
  
  assert "\n" + res == '''
(div "attr1"="value1" | (div "attr1"="value1"
  (p "id"="p_id")     |   (span "id"="s_id")
   ^ ^^^^^^^^^^^      |
  ! ** MISSING ** !   |   (p "id"="p_id")
)                     | )'''


def test_i_can_see_the_diff_when_matching():
  actual = Div(attr1="value1")
  expected = Div(attr1=Contains("value2"))
  
  with pytest.raises(AssertionError) as exc_info:
    matches(actual, expected)
  
  debug_output = str(exc_info.value)
  assert "\n" + debug_output == """
Path  : 'div'
Error : The condition 'Contains(value2)' is not satisfied.
(div "attr1"="value1") | (div "attr1"="Contains(value2)")
     ^^^^^^^^^^^^^^^^  |"""
