import uuid
from datetime import datetime, timedelta
from unittest import mock
from unittest.mock import patch

import pytest
from django.core.cache import cache
from django.db.models import ProtectedError
from django.utils import timezone

from sentry.issues.grouptype import FeedbackGroup, ProfileFileIOGroupType
from sentry.issues.occurrence_consumer import process_event_and_issue_occurrence
from sentry.models.group import Group, GroupStatus, get_group_with_redirect
from sentry.models.groupredirect import GroupRedirect
from sentry.models.grouprelease import GroupRelease
from sentry.models.groupsnooze import GroupSnooze
from sentry.models.release import Release, _get_cache_key
from sentry.replays.testutils import mock_replay
from sentry.testutils.cases import ReplaysSnubaTestCase, SnubaTestCase, TestCase
from sentry.testutils.helpers.datetime import before_now, iso_format
from sentry.testutils.helpers.features import with_feature
from sentry.testutils.silo import region_silo_test
from sentry.testutils.skips import requires_snuba
from sentry.types.group import GroupSubStatus
from tests.sentry.issues.test_utils import OccurrenceTestMixin

pytestmark = requires_snuba


@region_silo_test
class GroupTest(TestCase, SnubaTestCase):
    def setUp(self):
        super().setUp()
        self.min_ago = iso_format(before_now(minutes=1))

    def test_is_resolved(self):
        group = self.create_group(status=GroupStatus.RESOLVED)
        assert group.is_resolved()

        group.status = GroupStatus.IGNORED
        assert not group.is_resolved()

        group.status = GroupStatus.UNRESOLVED
        assert not group.is_resolved()

        group.last_seen = timezone.now() - timedelta(hours=12)

        group.project.update_option("sentry:resolve_age", 24)

        assert not group.is_resolved()

        group.project.update_option("sentry:resolve_age", 1)

        assert group.is_resolved()

    def test_is_ignored_with_expired_snooze(self):
        group = self.create_group(status=GroupStatus.IGNORED)
        GroupSnooze.objects.create(group=group, until=timezone.now() - timedelta(minutes=1))
        assert not group.is_ignored()

    def test_status_with_expired_snooze(self):
        group = self.create_group(status=GroupStatus.IGNORED)
        GroupSnooze.objects.create(group=group, until=timezone.now() - timedelta(minutes=1))
        assert group.get_status() == GroupStatus.UNRESOLVED

    def test_deleting_release_does_not_delete_group(self):
        project = self.create_project()
        release = Release.objects.create(version="a", organization_id=project.organization_id)
        release.add_project(project)
        group = self.create_group(project=project, first_release=release)

        with pytest.raises(ProtectedError):
            release.delete()

        group = Group.objects.get(id=group.id)
        assert group.first_release == release

    def test_save_truncate_message(self):
        assert len(self.create_group(message="x" * 300).message) == 255
        assert self.create_group(message="\nfoo\n   ").message == "foo"
        assert self.create_group(message="foo").message == "foo"
        assert self.create_group(message="").message == ""

    def test_get_group_with_redirect(self):
        group = self.create_group()
        assert get_group_with_redirect(group.id) == (group, False)

        duplicate_id = self.create_group().id
        Group.objects.filter(id=duplicate_id).delete()
        GroupRedirect.objects.create(group_id=group.id, previous_group_id=duplicate_id)

        assert get_group_with_redirect(duplicate_id) == (group, True)

        # We shouldn't end up in a case where the redirect points to a bad
        # reference, but testing this path for completeness.
        group.delete()

        with pytest.raises(Group.DoesNotExist):
            get_group_with_redirect(duplicate_id)

    def test_get_group_with_redirect_from_qualified_short_id(self):
        group = self.create_group()
        assert group.qualified_short_id
        assert get_group_with_redirect(
            group.qualified_short_id, organization=group.project.organization
        ) == (group, False)

        duplicate_group = self.create_group()
        duplicate_id = duplicate_group.id
        GroupRedirect.create_for_group(duplicate_group, group)
        Group.objects.filter(id=duplicate_id).delete()

        assert get_group_with_redirect(
            duplicate_group.qualified_short_id, organization=group.project.organization
        ) == (group, True)

        # We shouldn't end up in a case where the redirect points to a bad
        # reference, but testing this path for completeness.
        group.delete()

        with pytest.raises(Group.DoesNotExist):
            get_group_with_redirect(
                duplicate_group.qualified_short_id, organization=group.project.organization
            )

    def test_invalid_shared_id(self):
        with pytest.raises(Group.DoesNotExist):
            Group.objects.from_share_id("adc7a5b902184ce3818046302e94f8ec")

    def test_qualified_share_id(self):
        project = self.create_project(name="foo bar")
        group = self.create_group(project=project, short_id=project.next_short_id())
        short_id = group.qualified_short_id

        assert short_id.startswith("FOO-BAR-")

        group2 = Group.objects.by_qualified_short_id(group.organization.id, short_id)

        assert group2 == group

        with pytest.raises(Group.DoesNotExist):
            Group.objects.by_qualified_short_id(
                group.organization.id, "server_name:my-server-with-dashes-0ac14dadda3b428cf"
            )

        group.update(status=GroupStatus.PENDING_DELETION, substatus=None)
        with pytest.raises(Group.DoesNotExist):
            Group.objects.by_qualified_short_id(group.organization.id, short_id)

    def test_qualified_share_id_bulk(self):
        project = self.create_project(name="foo bar")
        group = self.create_group(project=project, short_id=project.next_short_id())
        group_2 = self.create_group(project=project, short_id=project.next_short_id())
        group_short_id = group.qualified_short_id
        group_2_short_id = group_2.qualified_short_id
        assert [group] == Group.objects.by_qualified_short_id_bulk(
            group.organization.id, [group_short_id]
        )
        assert {group, group_2} == set(
            Group.objects.by_qualified_short_id_bulk(
                group.organization.id,
                [group_short_id, group_2_short_id],
            )
        )

        group.update(status=GroupStatus.PENDING_DELETION, substatus=None)
        with pytest.raises(Group.DoesNotExist):
            Group.objects.by_qualified_short_id_bulk(
                group.organization.id, [group_short_id, group_2_short_id]
            )

    def test_first_last_release(self):
        project = self.create_project()
        release = Release.objects.create(version="a", organization_id=project.organization_id)
        event = self.store_event(
            data={"release": "a", "timestamp": self.min_ago}, project_id=project.id
        )
        group = event.group

        release = Release.objects.get(version="a")

        assert group.first_release == release
        assert group.get_first_release() == release.version
        cache.delete(_get_cache_key(group.id, group.project_id, True))
        assert group.get_last_release() == release.version

    def test_first_release_from_tag(self):
        project = self.create_project()
        event = self.store_event(
            data={"release": "a", "timestamp": self.min_ago}, project_id=project.id
        )

        group = event.group

        assert group.get_first_release() == "a"
        cache.delete(_get_cache_key(group.id, group.project_id, True))
        assert group.get_last_release() == "a"

    def test_first_last_release_miss(self):
        project = self.create_project()
        release = Release.objects.create(version="a", organization_id=project.organization_id)
        release.add_project(project)

        group = self.create_group(project=project)

        assert group.first_release is None
        assert group.get_first_release() is None
        assert group.get_last_release() is None

    def test_get_email_subject(self):
        project = self.create_project()
        group = self.create_group(project=project)

        expect = f"{group.qualified_short_id} - {group.title}"
        assert group.get_email_subject() == expect

    def test_get_absolute_url(self):
        for org_slug, group_id, params, expected in [
            ("org1", 23, None, "http://testserver/organizations/org1/issues/23/"),
            (
                "org2",
                42,
                {"environment": "dev"},
                "http://testserver/organizations/org2/issues/42/?environment=dev",
            ),
            (
                "\u00F6rg3",
                86,
                {"env\u00EDronment": "d\u00E9v"},
                "http://testserver/organizations/org3/issues/86/?env%C3%ADronment=d%C3%A9v",
            ),
        ]:
            org = self.create_organization(slug=org_slug)
            project = self.create_project(organization=org)
            group = self.create_group(id=group_id, project=project)
            actual = group.get_absolute_url(params)
            assert actual == expected

    def test_get_absolute_url_feedback(self):
        org_slug = "org1"
        org = self.create_organization(slug=org_slug)
        project = self.create_project(organization=org)
        group_id = 23
        params = None
        expected = f"http://testserver/organizations/org1/feedback/?feedbackSlug={project.slug}%3A23&project={project.id}"

        group = self.create_group(id=group_id, project=project, type=FeedbackGroup.type_id)
        actual = group.get_absolute_url(params)
        assert actual == expected

    def test_get_absolute_url_event(self):
        project = self.create_project()
        event = self.store_event(
            data={"fingerprint": ["group1"], "timestamp": self.min_ago}, project_id=project.id
        )
        group = event.group
        url = f"http://testserver/organizations/{project.organization.slug}/issues/{group.id}/events/{event.event_id}/"
        assert url == group.get_absolute_url(event_id=event.event_id)

    @with_feature("organizations:customer-domains")
    def test_get_absolute_url_customer_domains(self):
        project = self.create_project()
        event = self.store_event(
            data={"fingerprint": ["group1"], "timestamp": self.min_ago}, project_id=project.id
        )
        org = self.organization
        group = event.group
        expected = f"http://{org.slug}.testserver/issues/{group.id}/events/{event.event_id}/"
        assert expected == group.get_absolute_url(event_id=event.event_id)

        expected = f"http://{org.slug}.testserver/issues/{group.id}/"
        assert expected == group.get_absolute_url()

    def test_get_releases(self):
        now = timezone.now().replace(microsecond=0)
        project = self.create_project()
        group = self.create_group(project=project)
        group2 = self.create_group(project=project)

        last_release = Release.objects.create(
            organization_id=self.organization.id,
            version="100",
            date_added=iso_format(now - timedelta(seconds=10)),
        )
        first_release = Release.objects.create(
            organization_id=self.organization.id,
            version="200",
            date_added=iso_format(now - timedelta(seconds=100)),
        )
        GroupRelease.objects.create(
            project_id=project.id,
            group_id=group.id,
            release_id=first_release.id,
            environment="",
            last_seen=first_release.date_added,
            first_seen=first_release.date_added,
        )

        GroupRelease.objects.create(
            project_id=project.id,
            group_id=group.id,
            release_id=last_release.id,
            environment="",
            last_seen=last_release.date_added,
            first_seen=last_release.date_added,
        )

        assert group.get_first_release() == "200"
        cache.delete(_get_cache_key(group2.id, group2.project_id, True))

        assert group2.get_first_release() is None
        cache.delete(_get_cache_key(group.id, group.project_id, True))

        assert group.get_last_release() == "100"

        assert group2.get_last_release() is None

    def test_group_substatus_defaults(self):
        assert self.create_group(status=GroupStatus.UNRESOLVED).substatus is GroupSubStatus.ONGOING
        for nullable_status in (
            GroupStatus.IGNORED,
            GroupStatus.MUTED,
            GroupStatus.RESOLVED,
            GroupStatus.PENDING_DELETION,
            GroupStatus.DELETION_IN_PROGRESS,
            GroupStatus.REPROCESSING,
        ):
            assert self.create_group(status=nullable_status).substatus is None

    def test_group_valid_substatus(self):
        desired_status_substatus_pairs = [
            (GroupStatus.UNRESOLVED, GroupSubStatus.ESCALATING),
            (GroupStatus.UNRESOLVED, GroupSubStatus.REGRESSED),
            (GroupStatus.UNRESOLVED, GroupSubStatus.NEW),
            (GroupStatus.IGNORED, GroupSubStatus.FOREVER),
            (GroupStatus.IGNORED, GroupSubStatus.UNTIL_CONDITION_MET),
            (GroupStatus.IGNORED, GroupSubStatus.UNTIL_ESCALATING),
        ]
        for status, substatus in desired_status_substatus_pairs:
            group = self.create_group(status=status, substatus=substatus)
            assert group.substatus is substatus

    @patch("sentry.models.group.logger")
    def test_group_invalid_substatus(self, logger):
        status_substatus_pairs = [
            (GroupStatus.UNRESOLVED, GroupSubStatus.UNTIL_ESCALATING),
            (GroupStatus.IGNORED, GroupSubStatus.ONGOING),
            (GroupStatus.IGNORED, GroupSubStatus.ESCALATING),
        ]

        for status, substatus in status_substatus_pairs:
            self.create_group(status=status, substatus=substatus)

        assert logger.error.call_count == len(status_substatus_pairs)


@region_silo_test
class GroupIsOverResolveAgeTest(TestCase):
    def test_simple(self):
        group = self.group
        group.last_seen = timezone.now() - timedelta(hours=2)
        group.project.update_option("sentry:resolve_age", 1)  # 1 hour
        assert group.is_over_resolve_age() is True
        group.last_seen = timezone.now()
        assert group.is_over_resolve_age() is False


class GroupGetLatestEventTest(TestCase, OccurrenceTestMixin):
    def setUp(self):
        super().setUp()
        self.min_ago = iso_format(before_now(minutes=1))
        self.two_min_ago = iso_format(before_now(minutes=2))
        self.just_over_one_min_ago = iso_format(before_now(seconds=61))

    def test_get_latest_event_no_events(self):
        project = self.create_project()
        group = self.create_group(project=project)
        assert group.get_latest_event() is None

    def test_get_latest_event(self):
        self.store_event(
            data={"event_id": "a" * 32, "fingerprint": ["group-1"], "timestamp": self.two_min_ago},
            project_id=self.project.id,
        )
        self.store_event(
            data={"event_id": "b" * 32, "fingerprint": ["group-1"], "timestamp": self.min_ago},
            project_id=self.project.id,
        )

        group = Group.objects.first()

        group_event = group.get_latest_event()

        assert group_event.event_id == "b" * 32
        assert group_event.occurrence is None

    def test_get_latest_almost_identical_timestamps(self):
        self.store_event(
            data={
                "event_id": "a" * 32,
                "fingerprint": ["group-1"],
                "timestamp": self.just_over_one_min_ago,
            },
            project_id=self.project.id,
        )
        self.store_event(
            data={"event_id": "b" * 32, "fingerprint": ["group-1"], "timestamp": self.min_ago},
            project_id=self.project.id,
        )
        group = Group.objects.first()

        group_event = group.get_latest_event()

        assert group_event.event_id == "b" * 32
        assert group_event.occurrence is None

    def test_get_latest_event_occurrence(self):
        event_id = uuid.uuid4().hex
        occurrence_data = self.build_occurrence_data(event_id=event_id, project_id=self.project.id)
        occurrence = process_event_and_issue_occurrence(
            occurrence_data,
            {
                "event_id": event_id,
                "fingerprint": ["group-1"],
                "project_id": self.project.id,
                "timestamp": before_now(minutes=1).isoformat(),
            },
        )[0]

        group = Group.objects.first()
        group.update(type=ProfileFileIOGroupType.type_id)

        group_event = group.get_latest_event()
        assert group_event.event_id == event_id
        self.assert_occurrences_identical(group_event.occurrence, occurrence)


class GroupReplaysCacheTest(SnubaTestCase, ReplaysSnubaTestCase):
    def test_simple(self):
        replay1_id = "46eb3948be25448abd53fe36b5891ff2"
        self.project.flags.has_replays = True
        self.project.save()
        self.store_event(
            data={
                "message": "Hello world",
                "level": "error",
                "contexts": {"replay": {"replay_id": replay1_id}},
                "timestamp": iso_format(before_now(minutes=1)),
            },
            project_id=self.project.id,
        )

        self.store_replays(
            mock_replay(
                datetime.now() - timedelta(minutes=60),
                self.project.id,
                replay1_id,
            )
        )
        group = Group.objects.first()
        assert group.has_replays() is True

        # test caching
        with mock.patch(
            "sentry.models.group.metrics.incr",
        ) as incr:
            assert group.has_replays() is True
            incr.assert_any_call(
                "group.has_replays.cached",
                tags={
                    "has_replays": True,
                },
            )

    def test_no_replay_project(self):
        event = self.store_event(
            data={"fingerprint": ["group1"], "timestamp": iso_format(before_now(minutes=1))},
            project_id=self.project.id,
        )
        group = event.group
        assert group.has_replays() is False

    def test_no_replay_on_issue(self):
        self.project.flags.has_replays = True
        self.project.save()

        event = self.store_event(
            data={"fingerprint": ["group1"], "timestamp": iso_format(before_now(minutes=1))},
            project_id=self.project.id,
        )

        group = event.group
        assert group.has_replays() is False

        # test caching
        with mock.patch(
            "sentry.models.group.metrics.incr",
        ) as incr:
            assert group.has_replays() is False
            incr.assert_any_call(
                "group.has_replays.cached",
                tags={
                    "has_replays": False,
                },
            )
