"""
Integration tests for the Links feature.
Links are managed via Django template views, tested through the Django test client.
"""

import pytest

from links.models import Link, LinkChangeLog, Tag


@pytest.mark.django_db
class TestLinksUI:
    def test_list_page_returns_200(self, client):
        response = client.get("/", follow=True)
        assert response.status_code == 200

    def test_create_link(self, client):
        response = client.post(
            "/create/",
            {
                "alias": "testlink",
                "original_url": "https://example.com",
                "link_type": Link.LinkType.LINK,
                "description": "",
                "text": "",
            },
            follow=True,
        )
        assert response.status_code == 200
        assert Link.objects.filter(alias="testlink").exists()

    def test_redirect_follows_alias(self, client):
        Link.objects.create(alias="gh", original_url="https://github.com")
        response = client.get("/gh/")
        assert response.status_code == 302
        assert response["Location"] == "https://github.com"

    def test_redirect_unknown_alias_goes_to_create(self, client):
        response = client.get("/definitely-not-a-real-alias/")
        assert response.status_code == 302
        assert "create" in response["Location"]

    def test_delete_link(self, client):
        link = Link.objects.create(alias="del-me", original_url="https://example.com")
        response = client.post(f"/delete/{link.pk}/")
        assert response.status_code in (200, 302)
        assert not Link.objects.filter(pk=link.pk).exists()

    def test_link_with_template_url(self, client):
        Link.objects.create(
            alias="gosearch",
            original_url="https://google.com/search?q={query,default=hello}",
        )
        response = client.get("/gosearch/")
        assert response.status_code == 302
        assert "q=hello" in response["Location"]

    def test_link_with_template_param_override(self, client):
        Link.objects.create(
            alias="wiki",
            original_url="https://en.wikipedia.org/wiki/{topic,default=Python}",
        )
        response = client.get("/wiki/Django/")
        assert response.status_code == 302
        assert "Django" in response["Location"]


@pytest.mark.django_db
class TestTemplateLinkFormValidation:
    """Tests for template URL validation in LinkForm, including static Unicode text."""

    def test_create_link_with_unicode_prefix_in_template_url(self, client):
        """Static Chinese text before a template param should be accepted."""
        original_url = 'https://chatgpt.com/?q=翻译下面的段落为中文: {query,default=Cool beans}'
        response = client.post(
            "/create/",
            {
                "alias": "chatgpt-translate",
                "original_url": original_url,
                "link_type": Link.LinkType.LINK,
                "description": "",
                "text": "",
            },
            follow=True,
        )
        assert response.status_code == 200
        link = Link.objects.get(alias="chatgpt-translate")
        assert link.original_url == original_url

    def test_create_link_with_unicode_suffix_in_template_url(self, client):
        """Static Chinese text after a template param should be accepted."""
        original_url = 'https://chatgpt.com/?q={query,default=Cool beans}，保证真实性'
        response = client.post(
            "/create/",
            {
                "alias": "chatgpt-verify",
                "original_url": original_url,
                "link_type": Link.LinkType.LINK,
                "description": "",
                "text": "",
            },
            follow=True,
        )
        assert response.status_code == 200
        link = Link.objects.get(alias="chatgpt-verify")
        assert link.original_url == original_url

    def test_create_link_with_unicode_around_template_param(self, client):
        """Static Unicode text both before and after a template param should be accepted."""
        original_url = 'https://chatgpt.com/?q=翻译: {query,default=hello} 保证真实性'
        response = client.post(
            "/create/",
            {
                "alias": "chatgpt-both",
                "original_url": original_url,
                "link_type": Link.LinkType.LINK,
                "description": "",
                "text": "",
            },
            follow=True,
        )
        assert response.status_code == 200
        link = Link.objects.get(alias="chatgpt-both")
        assert link.original_url == original_url

    def test_create_link_with_invalid_url_still_rejected(self, client):
        """A completely invalid URL (no scheme/host) must still be rejected."""
        response = client.post(
            "/create/",
            {
                "alias": "bad-url",
                "original_url": "not-a-url-at-all",
                "link_type": Link.LinkType.LINK,
                "description": "",
                "text": "",
            },
            follow=True,
        )
        assert not Link.objects.filter(alias="bad-url").exists()


@pytest.mark.django_db
class TestLinkTagAutoCreate:
    """Select2 submits either existing tag PKs or raw text for new tags.
    New tags must be created server-side and lowercased."""

    def _create(self, client, tags):
        return client.post(
            "/create/",
            {
                "alias": "tagged-link",
                "original_url": "https://example.com",
                "link_type": Link.LinkType.LINK,
                "description": "",
                "text": "",
                "tags": tags,
            },
            follow=True,
        )

    def test_new_tag_is_created_and_lowercased(self, client):
        existing = Tag.objects.count()
        response = self._create(client, ["Python"])
        assert response.status_code == 200
        assert Link.objects.filter(alias="tagged-link").exists()

        link = Link.objects.get(alias="tagged-link")
        created = Tag.objects.exclude(pk__in=[t.pk for t in link.tags.all()])
        assert Tag.objects.count() == existing + 1
        tag = Tag.objects.filter(name__iexact="python").first()
        assert tag is not None
        assert tag.name == "python"
        assert tag.slug == "python"
        assert tag in link.tags.all()

    def test_multiple_new_tags_lowercased(self, client):
        before = Tag.objects.count()
        response = self._create(client, ["Python", "Django", "WEB"])
        assert response.status_code == 200

        link = Link.objects.get(alias="tagged-link")
        names = sorted(t.name for t in link.tags.all())
        assert names == ["django", "python", "web"]
        assert Tag.objects.count() == before + 3

    def test_existing_tag_reused_no_duplicate(self, client):
        existing = Tag.objects.create(name="python", slug="python")
        before = Tag.objects.count()

        response = self._create(client, ["Python"])
        assert response.status_code == 200
        link = Link.objects.get(alias="tagged-link")
        assert link.tags.count() == 1
        assert link.tags.first().pk == existing.pk
        # No duplicate tag created
        assert Tag.objects.count() == before
        assert Tag.objects.filter(name="python").count() == 1

    def test_existing_tag_pk_and_new_tag_mixed(self, client):
        existing = Tag.objects.create(name="existing", slug="existing")
        before = Tag.objects.count()

        response = self._create(client, [str(existing.pk), "NewTag"])
        assert response.status_code == 200
        link = Link.objects.get(alias="tagged-link")
        names = sorted(t.name for t in link.tags.all())
        assert names == ["existing", "newtag"]
        assert Tag.objects.count() == before + 1

    def test_no_tags_submitted_creates_no_tags(self, client):
        before = Tag.objects.count()
        response = self._create(client, [])
        assert response.status_code == 200
        link = Link.objects.get(alias="tagged-link")
        assert link.tags.count() == 0
        assert Tag.objects.count() == before

    def test_empty_and_whitespace_values_ignored(self, client):
        before = Tag.objects.count()
        response = self._create(client, ["", "  ", "real"])
        assert response.status_code == 200
        link = Link.objects.get(alias="tagged-link")
        assert link.tags.count() == 1
        assert link.tags.first().name == "real"
        assert Tag.objects.count() == before + 1

    def test_whitespace_in_tag_name_is_stripped(self, client):
        before = Tag.objects.count()
        # Select2's createTag trims, but be defensive server-side too
        response = self._create(client, ["  spaced out  "])
        assert response.status_code == 200
        link = Link.objects.get(alias="tagged-link")
        tag = link.tags.first()
        assert tag.name == "spaced out"
        assert tag.slug == "spaced-out"
        assert Tag.objects.count() == before + 1


@pytest.mark.django_db
class TestCustomLinkEditUrlChangeLog:
    """Editing a CUSTOM link's content (text) must NOT log a spurious
    "URL changed" entry, because custom-link URLs are derived from the
    alias and the original_url form field is hidden for custom links.
    Regression test for the bug where every edit of a custom link
    produced a LinkChangeLog row.

    A CONTENT_EDIT entry, however, SHOULD be created whenever the text
    changes (for either custom or regular links). The actual content is
    NOT persisted in the log — only a marker that it changed."""

    def _update(self, client, link, **payload):
        data = {
            "alias": link.alias,
            # The original_url input is hidden for custom links and not
            # submitted by the browser, so keep it blank in this helper.
            "original_url": "",
            "link_type": Link.LinkType.CUSTOM,
            "description": "",
            "text": "",
        }
        data.update(payload)
        return client.post(f"/link/{link.pk}/edit/", data, follow=True)

    def test_custom_link_text_edit_creates_no_url_change_log(self, client):
        link = Link.objects.create(
            alias="mynote",
            link_type=Link.LinkType.CUSTOM,
            original_url="/custom/mynote",
            text="old body text",
        )
        before_url_logs = LinkChangeLog.objects.filter(
            link=link, change_type=LinkChangeLog.ChangeType.URL_CHANGE
        ).count()
        before_content_logs = LinkChangeLog.objects.filter(
            link=link, change_type=LinkChangeLog.ChangeType.CONTENT_EDIT
        ).count()

        response = self._update(client, link, text="new body text")
        assert response.status_code == 200

        link.refresh_from_db()
        assert link.text == "new body text"
        assert link.original_url == "/custom/mynote"
        # No spurious URL-change log
        assert LinkChangeLog.objects.filter(
            link=link, change_type=LinkChangeLog.ChangeType.URL_CHANGE
        ).count() == before_url_logs
        # A content-edit log IS created, without persisting the content
        content_logs = LinkChangeLog.objects.filter(
            link=link, change_type=LinkChangeLog.ChangeType.CONTENT_EDIT
        ).order_by("-changed_at")
        assert content_logs.count() == before_content_logs + 1
        assert content_logs.first().metadata == {"changed": True}
        assert "new body text" not in str(content_logs.first().metadata)
        assert "old body text" not in str(content_logs.first().metadata)

    def test_regular_link_text_unchanged_creates_no_url_change_log(self, client):
        link = Link.objects.create(
            alias="gh",
            link_type=Link.LinkType.LINK,
            original_url="https://github.com",
        )
        before = LinkChangeLog.objects.filter(link=link).count()

        response = client.post(
            f"/link/{link.pk}/edit/",
            {
                "alias": "gh",
                "original_url": "https://github.com",
                "link_type": Link.LinkType.LINK,
                "description": "an edit that keeps the URL",
                "text": "",
            },
            follow=True,
        )
        assert response.status_code == 200
        assert LinkChangeLog.objects.filter(link=link).count() == before

    def test_custom_link_text_unchanged_creates_no_content_log(self, client):
        link = Link.objects.create(
            alias="mynote",
            link_type=Link.LinkType.CUSTOM,
            original_url="/custom/mynote",
            text="same body text",
        )
        before = LinkChangeLog.objects.filter(link=link).count()

        response = self._update(client, link, text="same body text")
        assert response.status_code == 200
        assert LinkChangeLog.objects.filter(link=link).count() == before

    def test_regular_link_text_change_creates_content_log(self, client):
        """Regular (LINK-type) links can also carry text content; editing that
        text should produce a CONTENT_EDIT entry too."""
        link = Link.objects.create(
            alias="gh",
            link_type=Link.LinkType.LINK,
            original_url="https://github.com",
            text="original notes",
        )
        before = LinkChangeLog.objects.filter(link=link).count()

        response = client.post(
            f"/link/{link.pk}/edit/",
            {
                "alias": "gh",
                "original_url": "https://github.com",
                "link_type": Link.LinkType.LINK,
                "description": "",
                "text": "edited notes",
            },
            follow=True,
        )
        assert response.status_code == 200
        content_logs = LinkChangeLog.objects.filter(
            link=link, change_type=LinkChangeLog.ChangeType.CONTENT_EDIT
        ).order_by("-changed_at")
        assert content_logs.count() == before + 1
        assert content_logs.first().metadata == {"changed": True}
        assert "edited notes" not in str(content_logs.first().metadata)

    def test_regular_link_url_change_logs_change(self, client):
        link = Link.objects.create(
            alias="gh",
            link_type=Link.LinkType.LINK,
            original_url="https://github.com",
        )
        before = LinkChangeLog.objects.filter(link=link).count()

        response = client.post(
            f"/link/{link.pk}/edit/",
            {
                "alias": "gh",
                "original_url": "https://github.com/junv",
                "link_type": Link.LinkType.LINK,
                "description": "",
                "text": "",
            },
            follow=True,
        )
        assert response.status_code == 200
        logs = LinkChangeLog.objects.filter(link=link).order_by("-changed_at")
        assert logs.count() == before + 1
        assert logs.first().old_url == "https://github.com"
        assert logs.first().new_url == "https://github.com/junv"

    def test_custom_link_alias_change_logs_url_change(self, client):
        """When the alias of a custom link changes, the derived URL changes
        too, and that should be logged once."""
        link = Link.objects.create(
            alias="mynote",
            link_type=Link.LinkType.CUSTOM,
            original_url="/custom/mynote",
            text="body text",
        )
        before = LinkChangeLog.objects.filter(link=link).count()

        # Helper defaults text to "" (custom links require non-empty text)
        response = self._update(client, link, alias="newnote", text="body text")
        assert response.status_code == 200

        link.refresh_from_db()
        assert link.alias == "newnote"
        assert link.original_url == "/custom/newnote"
        logs = LinkChangeLog.objects.filter(link=link).order_by("-changed_at")
        assert logs.count() == before + 1
        assert logs.first().old_url == "/custom/mynote"
        assert logs.first().new_url == "/custom/newnote"
