import re

import pytest
from django.template import Context
from django.template.base import Parser, Token, TokenType

from django_unicorn.components import UnicornView
from django_unicorn.errors import ComponentNotValidError
from django_unicorn.templatetags.unicorn import unicorn
from django_unicorn.utils import generate_checksum
from example.coffee.models import Flavor


class FakeModel:
    # Fake a model that only has an id

    def __init__(self):
        self.id = 178

    def to_json(self):
        return {"id": self.id}


class FakeComponent(UnicornView):
    template_name = "templates/test_component.html"


class FakeComponentParent(UnicornView):
    template_name = "templates/test_component_parent.html"


class FakeComponentParentImplicit(UnicornView):
    template_name = "templates/test_component_parent_implicit.html"


class FakeComponentChild(UnicornView):
    template_name = "templates/test_component_child.html"


class FakeComponentChildImplicit(UnicornView):
    template_name = "templates/test_component_child_implicit.html"

    def has_parent(self):
        return self.parent is not None


class FakeComponentArgs(UnicornView):
    template_name = "templates/test_component_args.html"
    hello = "world"

    def mount(self):
        self.hello = self.component_args[0]


class FakeComponentKwargs(UnicornView):
    template_name = "templates/test_component_kwargs.html"
    hello = "world"

    def mount(self):
        self.hello = self.component_kwargs.get("test_kwarg")


class FakeComponentKwargsWithHtmlEntity(UnicornView):
    template_name = "templates/test_component_kwargs_with_html_entity.html"
    hello = "world"

    def mount(self):
        self.hello = self.component_kwargs.get("test_kwarg")


class FakeComponentModel(UnicornView):
    template_name = "templates/test_component_model.html"
    model_id = None

    def mount(self):
        self.model_id = self.component_kwargs.get("model_id")


class FakeComponentCalls(UnicornView):
    template_name = "templates/test_component.html"

    def mount(self):
        self.call("testCall")


class FakeComponentCalls2(UnicornView):
    template_name = "templates/test_component.html"

    def mount(self):
        self.call("testCall2", "hello")


def test_unicorn_template_renders(client):
    response = client.get("/test")
    content = response.rendered_content.strip()

    assert response.wsgi_request.path == "/test"
    assert "WSGIRequest" in content
    assert content.startswith("<div unicorn:id")
    assert 'unicorn:name="tests.templatetags.test_unicorn_render.FakeComponentKwargs"' in content
    assert '<script type="application/json" id="unicorn:data:' in content


def test_unicorn_template_renders_with_parent_and_child(client):
    response = client.get("/test-parent")
    content = response.content.decode().strip()

    assert response.wsgi_request.path == "/test-parent"
    assert content.startswith("<div unicorn:id")
    assert 'unicorn:name="tests.templatetags.test_unicorn_render.FakeComponentParent"' in content
    assert 'unicorn:name="tests.templatetags.test_unicorn_render.FakeComponentChild"' in content
    assert "--parent--" in content
    assert "==child==" in content
    assert '<script type="application/json" id="unicorn:data:' in content


def test_unicorn_template_renders_with_parent_and_child_with_templateview(client):
    response = client.get("/test-parent-template")
    content = response.content.decode().strip()

    assert response.wsgi_request.path == "/test-parent-template"
    assert content.startswith("<div unicorn:id")
    assert 'unicorn:name="tests.templatetags.test_unicorn_render.FakeComponentParent"' in content
    assert 'unicorn:name="tests.templatetags.test_unicorn_render.FakeComponentChild"' in content
    assert "--parent--" in content
    assert "==child==" in content
    assert '<script type="application/json" id="unicorn:data:' in content


def test_unicorn_template_renders_with_implicit_parent_and_child(client):
    response = client.get("/test-parent-implicit")
    content = response.content.decode().strip()

    assert response.wsgi_request.path == "/test-parent-implicit"
    assert content.startswith("<div unicorn:id")
    assert 'unicorn:name="tests.templatetags.test_unicorn_render.FakeComponentParentImplicit"' in content
    assert 'unicorn:name="tests.templatetags.test_unicorn_render.FakeComponentChildImplicit"' in content
    assert "--parent--" in content
    assert "==child==" in content
    assert "has_parent:True" in content
    assert '<script type="application/json" id="unicorn:data:' in content


def test_unicorn_render_arg():
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentArgs' 'tested!'",
    )
    unicorn_node = unicorn(Parser([]), token)
    context = {}
    actual = unicorn_node.render(Context(context))

    assert "<b>tested!</b>" in actual


def test_unicorn_render_kwarg():
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargs' test_kwarg='tested!'",
    )
    unicorn_node = unicorn(Parser([]), token)
    context = {}
    actual = unicorn_node.render(Context(context))

    assert "<b>tested!</b>" in actual


def test_unicorn_render_invalid_kwarg():
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargs' test_kwarg=tested",
    )
    unicorn_node = unicorn(Parser([]), token)
    context = {}
    actual = unicorn_node.render(Context(context))

    assert "<b>None</b>" in actual


def test_unicorn_render_context_variable():
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargs' test_kwarg=test_var.nested",
    )
    unicorn_node = unicorn(Parser([]), token)
    context = {"test_var": {"nested": "variable!"}}
    actual = unicorn_node.render(Context(context))

    assert "<b>variable!</b>" in actual


def test_unicorn_render_with_invalid_html():
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargsWithHtmlEntity' test_kwarg=test_var.nested",
    )
    unicorn_node = unicorn(Parser([]), token)
    context = {"test_var": {"nested": "variable!"}}
    actual = unicorn_node.render(Context(context))

    assert "-&gt;variable!&lt;-" in actual


def test_unicorn_render_parent(settings):
    settings.DEBUG = True
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargs' parent=view",
    )
    unicorn_node = unicorn(Parser([]), token)
    view = FakeComponentParent(component_name="test", component_id="test_unicorn_render_parent")
    context = {"view": view}
    unicorn_node.render(Context(context))

    assert unicorn_node.parent
    assert (
        unicorn_node.component_id
        == "test_unicorn_render_parent:tests.templatetags.test_unicorn_render.FakeComponentKwargs"
    )


def test_unicorn_render_parent_with_key(settings):
    settings.DEBUG = True
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargs' parent=view key='blob'",
    )
    unicorn_node = unicorn(Parser([]), token)
    view = FakeComponentParent(component_name="test", component_id="test_unicorn_render_parent_with_key")
    context = {"view": view}
    unicorn_node.render(Context(context))

    assert (
        unicorn_node.component_id
        == "test_unicorn_render_parent_with_key:tests.templatetags.test_unicorn_render.FakeComponentKwargs:blob"
    )


def test_unicorn_render_parent_with_id(settings):
    settings.DEBUG = True
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargs' parent=view id='flob'",
    )
    unicorn_node = unicorn(Parser([]), token)
    view = FakeComponentParent(component_name="test", component_id="test_unicorn_render_parent_with_id")
    context = {"view": view}
    unicorn_node.render(Context(context))

    assert (
        unicorn_node.component_id
        == "test_unicorn_render_parent_with_id:tests.templatetags.test_unicorn_render.FakeComponentKwargs:flob"
    )


def test_unicorn_render_parent_with_pk(settings):
    settings.DEBUG = True
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargs' parent=view pk=99",
    )
    unicorn_node = unicorn(Parser([]), token)
    view = FakeComponentParent(component_name="test", component_id="test_unicorn_render_parent_with_pk")
    context = {"view": view}
    unicorn_node.render(Context(context))

    assert (
        unicorn_node.component_id
        == "test_unicorn_render_parent_with_pk:tests.templatetags.test_unicorn_render.FakeComponentKwargs:99"
    )


def test_unicorn_render_parent_with_model_id(settings):
    settings.DEBUG = True
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargs' parent=view model=model",
    )
    unicorn_node = unicorn(Parser([]), token)
    view = FakeComponentParent(component_name="test", component_id="test_unicorn_render_parent_with_model_id")

    context = {"view": view, "model": FakeModel()}
    unicorn_node.render(Context(context))

    assert (
        unicorn_node.component_id
        == "test_unicorn_render_parent_with_model_id:tests.templatetags.test_unicorn_render.FakeComponentKwargs:178"
    )


@pytest.mark.django_db
def test_unicorn_render_parent_with_model_pk(settings):
    settings.DEBUG = True
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargs' parent=view model=model",
    )
    unicorn_node = unicorn(Parser([]), token)
    view = FakeComponentParent(component_name="test", component_id="test_unicorn_render_parent_with_model_pk")

    flavor = Flavor(pk=187)

    context = {"view": view, "model": flavor}
    unicorn_node.render(Context(context))

    assert (
        unicorn_node.component_id
        == "test_unicorn_render_parent_with_model_pk:tests.templatetags.test_unicorn_render.FakeComponentKwargs:187"
    )


def test_unicorn_render_id_use_pk():
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentModel' model_id=model.id",
    )
    unicorn_node = unicorn(Parser([]), token)
    context = {"model": {"pk": 123}}
    actual = unicorn_node.render(Context(context))

    assert "==123==" in actual


def test_unicorn_render_component_one_script_tag(settings):
    settings.DEBUG = True
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargs'",
    )
    unicorn_node = unicorn(Parser([]), token)
    context = {}
    html = unicorn_node.render(Context(context))

    assert '<script type="module"' in html
    assert len(re.findall('<script type="module"', html)) == 1


def test_unicorn_render_component_minify_html(settings):
    settings.DEBUG = True
    settings.UNICORN["MINIFY_HTML"] = True
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargs'",
    )
    unicorn_node = unicorn(Parser([]), token)
    context = {}
    html = unicorn_node.render(Context(context))

    assert "<script type=module" in html
    assert len(re.findall("<script type=module", html)) == 1

    settings.UNICORN["MINIFY_HTML"] = False


def test_unicorn_render_child_component_no_script_tag(settings):
    settings.DEBUG = True
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargs' parent=view",
    )
    unicorn_node = unicorn(Parser([]), token)
    view = FakeComponentParent(
        component_name="test-no-script", component_id="test_unicorn_render_child_component_no_script_tag"
    )
    context = {"view": view}
    html = unicorn_node.render(Context(context))

    assert "<script" not in html


def test_unicorn_render_parent_component_one_script_tag(settings):
    settings.DEBUG = True
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentParent'",
    )
    unicorn_node = unicorn(Parser([]), token)
    context = {}
    html = unicorn_node.render(Context(context))

    assert '<script type="module"' in html
    assert len(re.findall('<script type="module"', html)) == 1


def test_unicorn_render_calls(settings):
    settings.DEBUG = True
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentCalls'",
    )
    unicorn_node = unicorn(Parser([]), token)
    context = {}
    html = unicorn_node.render(Context(context))

    assert '<script type="module"' in html
    assert len(re.findall('<script type="module"', html)) == 1
    assert '"calls":[{"fn":"testCall","args":[]}]' in html


def test_unicorn_render_calls_with_arg(settings):
    settings.DEBUG = True
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentCalls2'",
    )
    unicorn_node = unicorn(Parser([]), token)
    context = {}
    html = unicorn_node.render(Context(context))

    assert '<script type="module"' in html
    assert len(re.findall('<script type="module"', html)) == 1
    assert '"calls":[{"fn":"testCall2","args":["hello"]}]' in html


def test_unicorn_render_calls_no_mount_call(settings):
    settings.DEBUG = True
    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentParent'",
    )
    unicorn_node = unicorn(Parser([]), token)
    context = {}
    html = unicorn_node.render(Context(context))

    assert '<script type="module"' in html
    assert len(re.findall('<script type="module"', html)) == 1
    assert '"calls":[]' in html


def test_unicorn_render_hash(settings):
    settings.DEBUG = True
    settings.UNICORN["SCRIPT_LOCATION"] = "after"

    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentParent'",
    )
    unicorn_node = unicorn(Parser([]), token)
    context = {}
    html = unicorn_node.render(Context(context))

    assert '<script type="module"' in html
    assert len(re.findall('<script type="module"', html)) == 1
    assert '"hash":"' in html

    # Assert that the content hash is correct
    script_idx = html.index("<script")
    rendered_content_without_inserted_after_script = html[:script_idx]
    expected_hash = generate_checksum(rendered_content_without_inserted_after_script)
    assert f'"hash":"{expected_hash}"' in html


def test_unicorn_render_hash_append(settings):
    settings.DEBUG = True
    settings.UNICORN["SCRIPT_LOCATION"] = "append"

    token = Token(
        TokenType.TEXT,
        "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentParent'",
    )
    unicorn_node = unicorn(Parser([]), token)
    context = {}
    html = unicorn_node.render(Context(context))

    assert '<script type="module"' in html
    assert len(re.findall('<script type="module"', html)) == 1
    assert '"hash":"' in html

    # Assert that the content hash is correct
    script_idx = html.index("<script")
    rendered_content_without_appended_script = html[:script_idx] + "</div>"
    expected_hash = generate_checksum(rendered_content_without_appended_script)
    assert f'"hash":"{expected_hash}"' in html


def test_unicorn_render_with_component_name_from_context():
    token = Token(
        TokenType.TEXT,
        "unicorn component_name",
    )
    unicorn_node = unicorn(Parser([]), token)
    context = {"component_name": "tests.templatetags.test_unicorn_render.FakeComponent"}
    html = unicorn_node.render(Context(context))

    assert '<script type="module"' in html
    assert len(re.findall('<script type="module"', html)) == 1


def test_unicorn_render_with_invalid_component_name_from_context():
    token = Token(
        TokenType.TEXT,
        "unicorn bad_component_name",
    )
    unicorn_node = unicorn(Parser([]), token)
    context = {"component_name": "tests.templatetags.test_unicorn_render.FakeComponent"}

    with pytest.raises(ComponentNotValidError) as e:
        unicorn_node.render(Context(context))

    assert (
        e.exconly()
        == "django_unicorn.errors.ComponentNotValidError: Component template is not valid: bad_component_name."
    )
