from datetime import timedelta
from functools import cached_property
from unittest import mock

import pytest
from django.utils import timezone

from sentry.issues.grouptype import ProfileFileIOGroupType
from sentry.models.environment import Environment
from sentry.models.release import Release
from sentry.models.releaseprojectenvironment import ReleaseProjectEnvironment, ReleaseStages
from sentry.search.events.constants import (
    RELEASE_STAGE_ALIAS,
    SEMVER_ALIAS,
    SEMVER_BUILD_ALIAS,
    SEMVER_PACKAGE_ALIAS,
)
from sentry.tagstore.exceptions import (
    GroupTagKeyNotFound,
    GroupTagValueNotFound,
    TagKeyNotFound,
    TagValueNotFound,
)
from sentry.tagstore.snuba.backend import SnubaTagStorage
from sentry.tagstore.types import GroupTagValue, TagValue
from sentry.testutils.abstract import Abstract
from sentry.testutils.cases import PerformanceIssueTestCase, SnubaTestCase, TestCase
from sentry.testutils.helpers.datetime import before_now, iso_format
from sentry.utils.eventuser import EventUser
from sentry.utils.samples import load_data
from tests.sentry.issues.test_utils import SearchIssueTestMixin

exception = {
    "values": [
        {
            "type": "ValidationError",
            "value": "Bad request",
            "stacktrace": {
                "frames": [
                    {
                        "function": "?",
                        "filename": "http://localhost:1337/error.js",
                        "lineno": 29,
                        "colno": 3,
                        "in_app": False,
                    }
                ]
            },
        }
    ]
}


class TagStorageTest(TestCase, SnubaTestCase, SearchIssueTestMixin, PerformanceIssueTestCase):
    def setUp(self):
        super().setUp()

        self.ts = SnubaTagStorage()

        self.proj1 = self.create_project()
        env1 = "test"
        env2 = "test2"
        self.env3 = Environment.objects.create(
            organization_id=self.proj1.organization_id, name="test3"
        )
        self.now = timezone.now().replace(microsecond=0)

        self.store_event(
            data={
                "event_id": "1" * 32,
                "message": "message 1",
                "platform": "python",
                "environment": env1,
                "fingerprint": ["group-1"],
                "timestamp": iso_format(self.now - timedelta(seconds=1)),
                "tags": {
                    "foo": "bar",
                    "baz": "quux",
                    "sentry:release": 100,
                    "sentry:user": "id:user1",
                },
                "user": {"id": "user1"},
                "exception": exception,
            },
            project_id=self.proj1.id,
        )

        self.proj1group1 = self.store_event(
            data={
                "event_id": "2" * 32,
                "message": "message 1",
                "platform": "python",
                "environment": env1,
                "fingerprint": ["group-1"],
                "timestamp": iso_format(self.now - timedelta(seconds=2)),
                "tags": {
                    "foo": "bar",
                    "baz": "quux",
                    "sentry:release": 200,
                    "sentry:user": "id:user2",
                },
                "user": {"id": "user2"},
                "exception": exception,
            },
            project_id=self.proj1.id,
        ).group

        self.proj1group2 = self.store_event(
            data={
                "event_id": "3" * 32,
                "message": "message 2",
                "platform": "python",
                "environment": env1,
                "fingerprint": ["group-2"],
                "timestamp": iso_format(self.now - timedelta(seconds=2)),
                "tags": {"browser": "chrome", "sentry:user": "id:user1"},
                "user": {"id": "user1"},
            },
            project_id=self.proj1.id,
        ).group

        self.store_event(
            data={
                "event_id": "4" * 32,
                "message": "message2",
                "platform": "python",
                "environment": env2,
                "fingerprint": ["group-1"],
                "timestamp": iso_format(self.now - timedelta(seconds=2)),
                "tags": {"foo": "bar"},
            },
            project_id=self.proj1.id,
        )

        self.proj1env1 = Environment.objects.get(name=env1)
        self.proj1env2 = Environment.objects.get(name=env2)

    @cached_property
    def perf_group_and_env(self):
        env_name = "test"
        env = Environment.objects.get(name=env_name)

        event_data = load_data("transaction-n-plus-one", timestamp=before_now(minutes=10))
        event_data["environment"] = env_name

        event = self.create_performance_issue(
            event_data={
                **event_data,
                "event_id": "a" * 32,
                "timestamp": iso_format(self.now - timedelta(seconds=1)),
                "start_timestamp": iso_format(self.now - timedelta(seconds=1)),
                "tags": {"foo": "bar", "biz": "baz"},
                "release": "releaseme",
            }
        )
        self.create_performance_issue(
            event_data={
                **event_data,
                "event_id": "b" * 32,
                "timestamp": iso_format(self.now - timedelta(seconds=2)),
                "start_timestamp": iso_format(self.now - timedelta(seconds=2)),
                "tags": {"foo": "quux"},
                "release": "releaseme",
            }
        )
        perf_group = event.group
        return perf_group, env

    @cached_property
    def generic_group_and_env(self):
        env = Environment.objects.get(name="test")
        _, _, group_info = self.store_search_issue(
            self.project.id,
            self.user.id,
            [f"{ProfileFileIOGroupType.type_id}-group1"],
            env.name,
            timezone.now().replace(hour=0, minute=0, second=0) + timedelta(minutes=1),
            [("foo", "bar"), ("biz", "baz")],
            "releaseme",
        )
        assert group_info is not None
        return group_info.group, env

    def test_get_group_tag_keys_and_top_values(self):
        result = list(
            self.ts.get_group_tag_keys_and_top_values(
                self.proj1group1,
                [self.proj1env1.id],
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )
        )
        tags = [r.key for r in result]
        assert set(tags) == {"foo", "baz", "environment", "sentry:release", "sentry:user", "level"}

        result.sort(key=lambda r: r.key)
        assert result[0].key == "baz"
        assert result[0].top_values[0].value == "quux"
        assert result[0].count == 2

        assert result[4].key == "sentry:release"
        assert result[4].count == 2
        top_release_values = result[4].top_values
        assert len(top_release_values) == 2
        assert {v.value for v in top_release_values} == {"100", "200"}
        assert all(v.times_seen == 1 for v in top_release_values)

        # Now with only a specific set of keys,
        result = list(
            self.ts.get_group_tag_keys_and_top_values(
                self.proj1group1,
                [self.proj1env1.id],
                keys=["environment", "sentry:release"],
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )
        )
        tags = [r.key for r in result]
        assert set(tags) == {"environment", "sentry:release"}

        result.sort(key=lambda r: r.key)
        assert result[0].key == "environment"
        assert result[0].top_values[0].value == "test"

        assert result[1].key == "sentry:release"
        top_release_values = result[1].top_values
        assert len(top_release_values) == 2
        assert {v.value for v in top_release_values} == {"100", "200"}
        assert all(v.times_seen == 1 for v in top_release_values)

    def test_get_group_tag_keys_and_top_values_perf_issue(self):
        perf_group, env = self.perf_group_and_env

        result = list(
            self.ts.get_group_tag_keys_and_top_values(
                perf_group,
                [env.id],
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )
        )
        tags = [r.key for r in result]
        assert set(tags) == {
            "biz",
            "browser",
            "browser.name",
            "client_os",
            "client_os.name",
            "device",
            "device.family",
            "environment",
            "foo",
            "level",
            "runtime",
            "runtime.name",
            "sentry:release",
            "sentry:user",
            "transaction",
            "url",
        }

        result.sort(key=lambda r: r.key)
        assert result[0].key == "biz"
        assert result[0].top_values[0].value == "baz"
        assert result[0].count == 1

        assert result[12].key == "sentry:release"
        assert result[12].count == 2
        top_release_values = result[12].top_values
        assert len(top_release_values) == 1
        assert {v.value for v in top_release_values} == {"releaseme"}
        assert all(v.times_seen == 2 for v in top_release_values)

        # Now with only a specific set of keys,
        result = list(
            self.ts.get_group_tag_keys_and_top_values(
                perf_group,
                [env.id],
                keys=["environment", "sentry:release"],
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )
        )
        tags = [r.key for r in result]
        assert set(tags) == {"environment", "sentry:release"}

        result.sort(key=lambda r: r.key)
        assert result[0].key == "environment"
        assert result[0].top_values[0].value == "test"

        assert result[1].key == "sentry:release"
        top_release_values = result[1].top_values
        assert len(top_release_values) == 1
        assert {v.value for v in top_release_values} == {"releaseme"}
        assert all(v.times_seen == 2 for v in top_release_values)

    def test_get_group_tag_keys_and_top_values_generic_issue(self):
        group, env = self.generic_group_and_env
        result = list(
            self.ts.get_group_tag_keys_and_top_values(
                group, [env.id], tenant_ids={"referrer": "r", "organization_id": 1234}
            )
        )
        tags = [r.key for r in result]
        assert set(tags) == {"foo", "biz", "environment", "sentry:user", "level", "sentry:release"}

        result.sort(key=lambda r: r.key)
        assert result[0].key == "biz"
        assert result[0].top_values[0].value == "baz"
        assert result[0].count == 1

        assert result[4].key == "sentry:release"
        assert result[4].count == 1
        top_release_values = result[4].top_values
        assert len(top_release_values) == 1
        assert {v.value for v in top_release_values} == {"releaseme"}
        assert all(v.times_seen == 1 for v in top_release_values)

        # Now with only a specific set of keys,
        result = list(
            self.ts.get_group_tag_keys_and_top_values(
                group,
                [env.id],
                keys=["environment", "sentry:release"],
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )
        )
        tags = [r.key for r in result]
        assert set(tags) == {"environment", "sentry:release"}

        result.sort(key=lambda r: r.key)
        assert result[0].key == "environment"
        assert result[0].top_values[0].value == "test"

        assert result[1].key == "sentry:release"
        top_release_values = result[1].top_values
        assert len(top_release_values) == 1
        assert {v.value for v in top_release_values} == {"releaseme"}
        assert all(v.times_seen == 1 for v in top_release_values)
        # assert False

    def test_get_top_group_tag_values(self):
        resp = self.ts.get_top_group_tag_values(
            self.proj1group1,
            self.proj1env1.id,
            "foo",
            1,
            tenant_ids={"referrer": "r", "organization_id": 1234},
        )
        assert len(resp) == 1
        assert resp[0].times_seen == 2
        assert resp[0].key == "foo"
        assert resp[0].value == "bar"
        assert resp[0].group_id == self.proj1group1.id

    def test_get_top_group_tag_values_perf(self):
        perf_group, env = self.perf_group_and_env
        resp = self.ts.get_top_group_tag_values(
            perf_group,
            env.id,
            "foo",
            2,
            tenant_ids={"referrer": "r", "organization_id": 1234},
        )
        assert len(resp) == 2
        assert resp[0].times_seen == 1
        assert resp[0].key == "foo"
        assert resp[0].value == "bar"
        assert resp[0].group_id == perf_group.id
        assert resp[1].times_seen == 1
        assert resp[1].key == "foo"
        assert resp[1].value == "quux"
        assert resp[1].group_id == perf_group.id

    def test_get_top_group_tag_values_generic(self):
        group, env = self.generic_group_and_env
        resp = self.ts.get_top_group_tag_values(
            group, env.id, "foo", 1, tenant_ids={"referrer": "r", "organization_id": 1234}
        )
        assert len(resp) == 1
        assert resp[0].times_seen == 1
        assert resp[0].key == "foo"
        assert resp[0].value == "bar"
        assert resp[0].group_id == group.id

    def test_get_group_tag_value_count(self):
        assert (
            self.ts.get_group_tag_value_count(
                self.proj1group1,
                self.proj1env1.id,
                "foo",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )
            == 2
        )

    def test_get_group_tag_value_count_perf(self):
        perf_group, env = self.perf_group_and_env

        assert (
            self.ts.get_group_tag_value_count(
                perf_group, env.id, "foo", {"referrer": "r", "organization_id": 1234}
            )
            == 2
        )

    def test_get_group_tag_value_count_generic(self):
        group, env = self.generic_group_and_env

        assert (
            self.ts.get_group_tag_value_count(
                group, env.id, "foo", {"referrer": "r", "organization_id": 1234}
            )
            == 1
        )

    def test_get_tag_keys(self):
        expected_keys = {
            "baz",
            "browser",
            "environment",
            "foo",
            "sentry:release",
            "sentry:user",
            "level",
        }
        keys = {
            k.key: k
            for k in self.ts.get_tag_keys(
                project_id=self.proj1.id,
                environment_id=self.proj1env1.id,
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )
        }
        assert set(keys) == expected_keys
        keys = {
            k.key: k
            for k in self.ts.get_tag_keys(
                project_id=self.proj1.id,
                environment_id=self.proj1env1.id,
                include_values_seen=True,
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )
        }
        assert set(keys) == expected_keys

    def test_get_tag_keys_removed_from_denylist(self):
        denylist_keys = frozenset(["browser", "sentry:release"])
        expected_keys = {
            "baz",
            "environment",
            "foo",
            "sentry:user",
            "level",
        }
        keys = {
            k.key: k
            for k in self.ts.get_tag_keys(
                project_id=self.proj1.id,
                environment_id=self.proj1env1.id,
                denylist=denylist_keys,
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )
        }
        assert set(keys) == expected_keys
        keys = {
            k.key: k
            for k in self.ts.get_tag_keys(
                project_id=self.proj1.id,
                environment_id=self.proj1env1.id,
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )
        }
        expected_keys |= {"browser", "sentry:release"}
        assert set(keys) == expected_keys

    def test_get_group_tag_key(self):
        with pytest.raises(GroupTagKeyNotFound):
            self.ts.get_group_tag_key(
                group=self.proj1group1,
                environment_id=self.proj1env1.id,
                key="notreal",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )

        assert (
            self.ts.get_group_tag_key(
                group=self.proj1group1,
                environment_id=self.proj1env1.id,
                key="foo",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            ).key
            == "foo"
        )

        keys = {
            k.key: k
            for k in self.ts.get_group_tag_keys(
                self.proj1group1,
                [self.proj1env1.id],
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )
        }
        assert set(keys) == {"baz", "environment", "foo", "sentry:release", "sentry:user", "level"}

    def test_get_group_tag_key_perf(self):
        perf_group, env = self.perf_group_and_env

        with pytest.raises(GroupTagKeyNotFound):
            self.ts.get_group_tag_key(
                group=perf_group,
                environment_id=env.id,
                key="notreal",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )

        assert (
            self.ts.get_group_tag_key(
                group=perf_group,
                environment_id=self.proj1env1.id,
                key="foo",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            ).key
            == "foo"
        )
        keys = {
            k.key: k
            for k in self.ts.get_group_tag_keys(
                perf_group,
                [env.id],
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )
        }
        assert set(keys) == {
            "biz",
            "browser",
            "browser.name",
            "client_os",
            "client_os.name",
            "device",
            "device.family",
            "environment",
            "foo",
            "level",
            "runtime",
            "runtime.name",
            "sentry:release",
            "sentry:user",
            "transaction",
            "url",
        }

    def test_get_group_tag_key_generic(self):
        group, env = self.generic_group_and_env

        with pytest.raises(GroupTagKeyNotFound):
            self.ts.get_group_tag_key(
                group=group,
                environment_id=env.id,
                key="notreal",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )

        assert (
            self.ts.get_group_tag_key(
                group=group,
                environment_id=self.proj1env1.id,
                key="foo",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            ).key
            == "foo"
        )

        keys = {
            k.key: k
            for k in self.ts.get_group_tag_keys(
                group, [env.id], tenant_ids={"referrer": "r", "organization_id": 1234}
            )
        }
        assert set(keys) == {"biz", "environment", "foo", "sentry:user", "level", "sentry:release"}

    def test_get_group_tag_value(self):
        with pytest.raises(GroupTagValueNotFound):
            self.ts.get_group_tag_value(
                project_id=self.proj1.id,
                group_id=self.proj1group1.id,
                environment_id=self.proj1env1.id,
                key="foo",
                value="notreal",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )

        assert (
            self.ts.get_group_tag_values(
                group=self.proj1group1,
                environment_id=self.proj1env1.id,
                key="notreal",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )
            == set()
        )

        assert (
            list(
                self.ts.get_group_tag_values(
                    group=self.proj1group1,
                    environment_id=self.proj1env1.id,
                    tenant_ids={"referrer": "r", "organization_id": 1234},
                    key="foo",
                )
            )[0].value
            == "bar"
        )

        assert (
            self.ts.get_group_tag_value(
                project_id=self.proj1.id,
                group_id=self.proj1group1.id,
                environment_id=self.proj1env1.id,
                key="foo",
                value="bar",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            ).value
            == "bar"
        )

    def test_get_tag_key(self):
        with pytest.raises(TagKeyNotFound):
            self.ts.get_tag_key(
                project_id=self.proj1.id,
                environment_id=self.proj1env1.id,
                key="notreal",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )

    def test_get_tag_value(self):
        with pytest.raises(TagValueNotFound):
            self.ts.get_tag_value(
                project_id=self.proj1.id,
                environment_id=self.proj1env1.id,
                key="foo",
                value="notreal",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )

    def test_get_tag_value_label(self):
        assert self.ts.get_tag_value_label("foo", "notreal") == "notreal"
        assert self.ts.get_tag_value_label("sentry:user", None) is None
        assert self.ts.get_tag_value_label("sentry:user", "id:stuff") == "stuff"
        assert self.ts.get_tag_value_label("sentry:user", "email:stuff") == "stuff"
        assert self.ts.get_tag_value_label("sentry:user", "username:stuff") == "stuff"
        assert self.ts.get_tag_value_label("sentry:user", "ip:stuff") == "stuff"

    def test_get_groups_user_counts(self):
        assert self.ts.get_groups_user_counts(
            project_ids=[self.proj1.id],
            group_ids=[self.proj1group1.id, self.proj1group2.id],
            environment_ids=[self.proj1env1.id],
            tenant_ids={"referrer": "r", "organization_id": 1234},
        ) == {self.proj1group1.id: 2, self.proj1group2.id: 1}

        # test filtering by date range where there shouldn't be results
        assert (
            self.ts.get_groups_user_counts(
                project_ids=[self.proj1.id],
                group_ids=[self.proj1group1.id, self.proj1group2.id],
                environment_ids=[self.proj1env1.id],
                start=self.now - timedelta(days=5),
                end=self.now - timedelta(days=4),
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )
            == {}
        )

    def test_get_groups_user_counts_no_environments(self):
        self.store_event(
            data={
                "event_id": "3" * 32,
                "message": "message 1",
                "platform": "python",
                "fingerprint": ["group-1"],
                "timestamp": iso_format(self.now - timedelta(seconds=1)),
                "tags": {
                    "foo": "bar",
                    "baz": "quux",
                    "sentry:release": 100,
                    "sentry:user": "id:user3",
                },
                "user": {"id": "user3"},
                "exception": exception,
            },
            project_id=self.proj1.id,
        )
        assert self.ts.get_groups_user_counts(
            project_ids=[self.proj1.id],
            group_ids=[self.proj1group1.id, self.proj1group2.id],
            environment_ids=None,
            tenant_ids={"referrer": "r", "organization_id": 1234},
        ) == {self.proj1group1.id: 3, self.proj1group2.id: 1}

    @mock.patch("sentry.analytics.record")
    def test_get_group_tag_values_for_users(self, mock_record):
        result = self.ts.get_group_tag_values_for_users(
            [
                EventUser(
                    project_id=self.proj1.id,
                    email=None,
                    username=None,
                    name=None,
                    ip_address=None,
                    user_ident="user1",
                )
            ],
            tenant_ids={"referrer": "r", "organization_id": 1234},
        )
        assert len(result) == 2
        assert {v.group_id for v in result} == {self.proj1group1.id, self.proj1group2.id}
        assert {v.last_seen for v in result} == {
            self.now - timedelta(seconds=1),
            self.now - timedelta(seconds=2),
        }
        result.sort(key=lambda x: x.last_seen)
        assert result[0].last_seen == self.now - timedelta(seconds=2)
        assert result[1].last_seen == self.now - timedelta(seconds=1)
        for v in result:
            assert v.value == "user1"

        result = self.ts.get_group_tag_values_for_users(
            [
                EventUser(
                    project_id=self.proj1.id,
                    email=None,
                    username=None,
                    name=None,
                    ip_address=None,
                    user_ident="user2",
                )
            ],
            tenant_ids={"referrer": "r", "organization_id": 1234},
        )
        assert len(result) == 1
        assert result[0].value == "user2"
        assert result[0].last_seen == self.now - timedelta(seconds=2)

        mock_record.assert_called_with(
            "eventuser_endpoint.request",
            project_id=self.proj1.id,
            endpoint="sentry.tagstore.snuba.backend.SnubaTagStorage.get_group_tag_values_for_users",
        )

    def test_get_release_tags(self):
        tags = list(
            self.ts.get_release_tags(self.proj1.organization_id, [self.proj1.id], None, ["100"])
        )

        assert len(tags) == 1
        one_second_ago = self.now - timedelta(seconds=1)
        assert tags[0].last_seen == one_second_ago
        assert tags[0].first_seen == one_second_ago
        assert tags[0].times_seen == 1
        assert tags[0].key == "sentry:release"

    def test_get_release_tags_uses_release_project_environment(self):
        tags = list(
            self.ts.get_release_tags(self.proj1.organization_id, [self.proj1.id], None, ["100"])
        )

        assert len(tags) == 1
        one_second_ago = self.now - timedelta(seconds=1)
        assert tags[0].last_seen == one_second_ago
        assert tags[0].first_seen == one_second_ago
        assert tags[0].times_seen == 1

        one_day_ago = self.now - timedelta(days=1)
        two_days_ago = self.now - timedelta(days=2)
        self.store_event(
            data={
                "event_id": "5" * 32,
                "message": "message3",
                "platform": "python",
                "environment": None,
                "fingerprint": ["group-1"],
                "timestamp": iso_format(one_day_ago),
                "tags": {
                    "sentry:release": 100,
                },
            },
            project_id=self.proj1.id,
        )

        release = Release.objects.create(version="100", organization=self.organization)
        ReleaseProjectEnvironment.objects.create(
            release_id=release.id,
            project_id=self.proj1.id,
            environment_id=self.env3.id,
            first_seen=one_day_ago,
        )

        self.store_event(
            data={
                "event_id": "6" * 32,
                "message": "message3",
                "platform": "python",
                "environment": None,
                "fingerprint": ["group-1"],
                "timestamp": iso_format(two_days_ago),
                "tags": {
                    "sentry:release": 100,
                },
            },
            project_id=self.proj1.id,
        )
        tags = list(
            self.ts.get_release_tags(self.proj1.organization_id, [self.proj1.id], None, ["100"])
        )
        assert tags[0].last_seen == one_second_ago
        assert tags[0].first_seen == one_day_ago
        assert (
            tags[0].times_seen == 2
        )  # Isn't 3 because start was limited by the ReleaseProjectEnvironment entry

    def test_get_group_event_filter(self):
        assert self.ts.get_group_event_filter(
            self.proj1.id,
            self.proj1group1.id,
            [self.proj1env1.id],
            {"foo": "bar"},
            None,
            None,
            tenant_ids={"referrer": "r", "organization_id": 1234},
        ) == {"event_id__in": {"1" * 32, "2" * 32}}

        assert self.ts.get_group_event_filter(
            self.proj1.id,
            self.proj1group1.id,
            [self.proj1env1.id],
            {"foo": "bar"},
            (self.now - timedelta(seconds=1)),
            None,
            tenant_ids={"referrer": "r", "organization_id": 1234},
        ) == {"event_id__in": {"1" * 32}}

        assert self.ts.get_group_event_filter(
            self.proj1.id,
            self.proj1group1.id,
            [self.proj1env1.id],
            {"foo": "bar"},
            None,
            (self.now - timedelta(seconds=1)),
            tenant_ids={"referrer": "r", "organization_id": 1234},
        ) == {"event_id__in": {"2" * 32}}

        assert self.ts.get_group_event_filter(
            self.proj1.id,
            self.proj1group1.id,
            [self.proj1env1.id, self.proj1env2.id],
            {"foo": "bar"},
            None,
            None,
            tenant_ids={"referrer": "r", "organization_id": 1234},
        ) == {"event_id__in": {"1" * 32, "2" * 32, "4" * 32}}

        assert self.ts.get_group_event_filter(
            self.proj1.id,
            self.proj1group1.id,
            [self.proj1env1.id],
            {"foo": "bar", "sentry:release": "200"},  # AND
            None,
            None,
            tenant_ids={"referrer": "r", "organization_id": 1234},
        ) == {"event_id__in": {"2" * 32}}

        assert self.ts.get_group_event_filter(
            self.proj1.id,
            self.proj1group2.id,
            [self.proj1env1.id],
            {"browser": "chrome"},
            None,
            None,
            tenant_ids={"referrer": "r", "organization_id": 1234},
        ) == {"event_id__in": {"3" * 32}}

        assert (
            self.ts.get_group_event_filter(
                self.proj1.id,
                self.proj1group2.id,
                [self.proj1env1.id],
                {"browser": "ie"},
                None,
                None,
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )
            is None
        )

    def test_get_tag_value_paginator(self):
        from sentry.tagstore.types import TagValue

        assert list(
            self.ts.get_tag_value_paginator(
                self.proj1.id,
                self.proj1env1.id,
                "sentry:user",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            ).get_result(10)
        ) == [
            TagValue(
                key="sentry:user",
                value="id:user1",
                times_seen=2,
                first_seen=self.now - timedelta(seconds=2),
                last_seen=self.now - timedelta(seconds=1),
            ),
            TagValue(
                key="sentry:user",
                value="id:user2",
                times_seen=1,
                first_seen=self.now - timedelta(seconds=2),
                last_seen=self.now - timedelta(seconds=2),
            ),
        ]

        assert list(
            self.ts.get_tag_value_paginator(
                self.proj1.id,
                self.proj1env1.id,
                "sentry:user",
                query="user1",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            ).get_result(10)
        ) == [
            TagValue(
                key="sentry:user",
                value="id:user1",
                times_seen=2,
                first_seen=self.now - timedelta(seconds=2),
                last_seen=self.now - timedelta(seconds=1),
            )
        ]

    def test_get_tag_value_paginator_with_dates(self):
        from sentry.tagstore.types import TagValue

        day_ago = self.now - timedelta(days=1)
        two_days_ago = self.now - timedelta(days=2)
        assert list(
            self.ts.get_tag_value_paginator(
                self.proj1.id,
                self.proj1env1.id,
                "sentry:user",
                start=day_ago,
                end=self.now,
                tenant_ids={"referrer": "r", "organization_id": 1234},
            ).get_result(10)
        ) == [
            TagValue(
                key="sentry:user",
                value="id:user1",
                times_seen=2,
                first_seen=self.now - timedelta(seconds=2),
                last_seen=self.now - timedelta(seconds=1),
            ),
            TagValue(
                key="sentry:user",
                value="id:user2",
                times_seen=1,
                first_seen=self.now - timedelta(seconds=2),
                last_seen=self.now - timedelta(seconds=2),
            ),
        ]

        day_ago = self.now - timedelta(days=1)
        assert (
            list(
                self.ts.get_tag_value_paginator(
                    self.proj1.id,
                    self.proj1env1.id,
                    "sentry:user",
                    start=two_days_ago,
                    end=day_ago,
                    tenant_ids={"referrer": "r", "organization_id": 1234},
                ).get_result(10)
            )
            == []
        )

    def test_numeric_tag_value_paginator(self):
        from sentry.tagstore.types import TagValue

        assert list(
            self.ts.get_tag_value_paginator(
                self.proj1.id,
                self.proj1env1.id,
                "stack.lineno",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            ).get_result(10)
        ) == [
            TagValue(
                key="stack.lineno",
                value="29",
                times_seen=2,
                first_seen=self.now - timedelta(seconds=2),
                last_seen=self.now - timedelta(seconds=1),
            )
        ]

        assert list(
            self.ts.get_tag_value_paginator(
                self.proj1.id,
                self.proj1env1.id,
                "stack.lineno",
                query="30",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            ).get_result(10)
        ) == [
            TagValue(
                key="stack.lineno",
                value="29",
                times_seen=2,
                first_seen=self.now - timedelta(seconds=2),
                last_seen=self.now - timedelta(seconds=1),
            )
        ]

    def test_get_group_tag_value_iter(self):
        from sentry.tagstore.types import GroupTagValue

        assert list(
            self.ts.get_group_tag_value_iter(
                self.proj1group1,
                [self.proj1env1.id],
                "sentry:user",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )
        ) == [
            GroupTagValue(
                group_id=self.proj1group1.id,
                key="sentry:user",
                value="id:user1",
                times_seen=1,
                first_seen=self.now - timedelta(seconds=1),
                last_seen=self.now - timedelta(seconds=1),
            ),
            GroupTagValue(
                group_id=self.proj1group1.id,
                key="sentry:user",
                value="id:user2",
                times_seen=1,
                first_seen=self.now - timedelta(seconds=2),
                last_seen=self.now - timedelta(seconds=2),
            ),
        ]

    def test_get_group_tag_value_iter_perf(self):
        from sentry.tagstore.types import GroupTagValue

        group, env = self.perf_group_and_env

        assert list(
            self.ts.get_group_tag_value_iter(
                group,
                [env.id],
                "foo",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )
        ) == [
            GroupTagValue(
                group_id=group.id,
                key="foo",
                value="bar",
                times_seen=1,
                first_seen=self.now - timedelta(seconds=1),
                last_seen=self.now - timedelta(seconds=1),
            ),
            GroupTagValue(
                group_id=group.id,
                key="foo",
                value="quux",
                times_seen=1,
                first_seen=self.now - timedelta(seconds=2),
                last_seen=self.now - timedelta(seconds=2),
            ),
        ]

    def test_get_group_tag_value_paginator(self):
        from sentry.tagstore.types import GroupTagValue

        assert list(
            self.ts.get_group_tag_value_paginator(
                self.proj1group1,
                [self.proj1env1.id],
                "sentry:user",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            ).get_result(10)
        ) == [
            GroupTagValue(
                group_id=self.proj1group1.id,
                key="sentry:user",
                value="id:user1",
                times_seen=1,
                first_seen=self.now - timedelta(seconds=1),
                last_seen=self.now - timedelta(seconds=1),
            ),
            GroupTagValue(
                group_id=self.proj1group1.id,
                key="sentry:user",
                value="id:user2",
                times_seen=1,
                first_seen=self.now - timedelta(seconds=2),
                last_seen=self.now - timedelta(seconds=2),
            ),
        ]

    def test_get_group_tag_value_paginator_perf(self):
        from sentry.tagstore.types import GroupTagValue

        group, env = self.perf_group_and_env

        assert list(
            self.ts.get_group_tag_value_paginator(
                group,
                [env.id],
                "foo",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            ).get_result(10)
        ) == [
            GroupTagValue(
                group_id=group.id,
                key="foo",
                value="bar",
                times_seen=1,
                first_seen=self.now - timedelta(seconds=1),
                last_seen=self.now - timedelta(seconds=1),
            ),
            GroupTagValue(
                group_id=group.id,
                key="foo",
                value="quux",
                times_seen=1,
                first_seen=self.now - timedelta(seconds=2),
                last_seen=self.now - timedelta(seconds=2),
            ),
        ]

    def test_get_group_tag_value_paginator_times_seen(self):
        from sentry.tagstore.types import GroupTagValue

        self.store_event(
            data={
                "event_id": "5" * 32,
                "message": "message 1",
                "platform": "python",
                "environment": self.proj1env1.name,
                "fingerprint": ["group-1"],
                "timestamp": iso_format(self.now - timedelta(seconds=2)),
                "tags": {
                    "foo": "bar",
                    "baz": "quux",
                    "sentry:release": 100,
                    "sentry:user": "id:user2",
                },
                "user": {"id": "user2"},
                "exception": exception,
            },
            project_id=self.proj1.id,
        )

        assert list(
            self.ts.get_group_tag_value_paginator(
                self.proj1group1,
                [self.proj1env1.id],
                "sentry:user",
                order_by="-times_seen",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            ).get_result(10)
        ) == [
            GroupTagValue(
                group_id=self.proj1group1.id,
                key="sentry:user",
                value="id:user2",
                times_seen=2,
                first_seen=self.now - timedelta(seconds=2),
                last_seen=self.now - timedelta(seconds=2),
            ),
            GroupTagValue(
                group_id=self.proj1group1.id,
                key="sentry:user",
                value="id:user1",
                times_seen=1,
                first_seen=self.now - timedelta(seconds=1),
                last_seen=self.now - timedelta(seconds=1),
            ),
        ]

    def test_get_group_tag_value_paginator_times_seen_perf(self):
        from sentry.tagstore.types import GroupTagValue

        group, env = self.perf_group_and_env

        event_data = load_data("transaction-n-plus-one", timestamp=before_now(minutes=10))

        self.create_performance_issue(
            event_data={
                **event_data,
                "event_id": "a" * 32,
                "timestamp": iso_format(self.now - timedelta(seconds=1)),
                "start_timestamp": iso_format(self.now - timedelta(seconds=1)),
                "tags": {"foo": "bar", "biz": "baz"},
                "release": "releaseme",
                "environment": env.name,
            }
        )

        assert list(
            self.ts.get_group_tag_value_paginator(
                group,
                [env.id],
                "foo",
                order_by="-times_seen",
                tenant_ids={"referrer": "r", "organization_id": 1234},
            ).get_result(10)
        ) == [
            GroupTagValue(
                group_id=group.id,
                key="foo",
                value="bar",
                times_seen=2,
                first_seen=self.now - timedelta(seconds=1),
                last_seen=self.now - timedelta(seconds=1),
            ),
            GroupTagValue(
                group_id=group.id,
                key="foo",
                value="quux",
                times_seen=1,
                first_seen=self.now - timedelta(seconds=2),
                last_seen=self.now - timedelta(seconds=2),
            ),
        ]

    def test_get_group_seen_values_for_environments(self):
        assert self.ts.get_group_seen_values_for_environments(
            [self.proj1.id],
            [self.proj1group1.id],
            [self.proj1env1.id],
            tenant_ids={"referrer": "r", "organization_id": 1234},
        ) == {
            self.proj1group1.id: {
                "first_seen": self.now - timedelta(seconds=2),
                "last_seen": self.now - timedelta(seconds=1),
                "times_seen": 2,
            }
        }

        # test where there should be no results because of time filters
        assert (
            self.ts.get_group_seen_values_for_environments(
                [self.proj1.id],
                [self.proj1group1.id],
                [self.proj1env1.id],
                start=self.now - timedelta(hours=5),
                end=self.now - timedelta(hours=4),
                tenant_ids={"referrer": "r", "organization_id": 1234},
            )
            == {}
        )


class ProfilingTagStorageTest(TestCase, SnubaTestCase, SearchIssueTestMixin):
    def setUp(self):
        super().setUp()
        self.ts = SnubaTagStorage()

    def test_get_profiling_groups_user_counts_simple(self):
        first_group_fingerprint = f"{ProfileFileIOGroupType.type_id}-group1"
        first_group_timestamp_start = timezone.now() - timedelta(days=5)

        self.store_search_issue(
            self.project.id,
            1,
            [first_group_fingerprint],
            self.environment.name,
            first_group_timestamp_start + timedelta(minutes=1),
        )

        self.store_search_issue(
            self.project.id,
            1,
            [first_group_fingerprint],
            self.environment.name,
            first_group_timestamp_start + timedelta(minutes=2),
        )

        self.store_search_issue(
            self.project.id,
            2,
            [first_group_fingerprint],
            self.environment.name,
            first_group_timestamp_start + timedelta(minutes=3),
        )

        event, issue_occurrence, group_info = self.store_search_issue(
            self.project.id,
            3,
            [first_group_fingerprint],
            None,
            first_group_timestamp_start + timedelta(minutes=4),
        )
        assert group_info is not None
        first_group = group_info.group

        second_group_fingerprint = f"{ProfileFileIOGroupType.type_id}-group2"
        second_group_timestamp_start = timezone.now() - timedelta(hours=5)
        for incr in range(1, 5):
            event, issue_occurrence, group_info = self.store_search_issue(
                self.project.id,
                incr,
                [second_group_fingerprint],
                self.environment.name if incr != 4 else None,
                second_group_timestamp_start + timedelta(minutes=incr),
            )
            assert group_info is not None
            second_group = group_info.group

        assert self.ts.get_generic_groups_user_counts(
            [self.project.id],
            group_ids=[first_group.id, second_group.id],
            environment_ids=[self.environment.id],
            tenant_ids={"referrer": "r", "organization_id": 1234},
        ) == {first_group.id: 2, second_group.id: 3}
        assert self.ts.get_generic_groups_user_counts(
            [self.project.id],
            group_ids=[first_group.id, second_group.id],
            environment_ids=None,
            tenant_ids={"referrer": "r", "organization_id": 1234},
        ) == {first_group.id: 3, second_group.id: 4}

    def test_get_profiling_group_list_tag_value_by_environment(self):
        group_fingerprint = f"{ProfileFileIOGroupType.type_id}-group1"
        start_timestamp = timezone.now() - timedelta(hours=1)
        first_event_ts = start_timestamp + timedelta(minutes=1)
        self.store_search_issue(
            self.project.id,
            1,
            [group_fingerprint],
            self.environment.name,
            first_event_ts,
        )
        last_event_ts = start_timestamp + timedelta(hours=1)
        event, occurrence, group_info = self.store_search_issue(
            self.project.id,
            1,
            [group_fingerprint],
            self.environment.name,
            last_event_ts,
        )
        assert group_info is not None
        group = group_info.group

        group_seen_stats = self.ts.get_generic_group_list_tag_value(
            [group.project_id],
            [group.id],
            [self.environment.id],
            "environment",
            self.environment.name,
            tenant_ids={"referrer": "r", "organization_id": 1234},
        )

        assert group_seen_stats == {
            group.id: GroupTagValue(
                key="environment",
                value=self.environment.name,
                group_id=group.id,
                times_seen=2,
                first_seen=first_event_ts.replace(microsecond=0),
                last_seen=last_event_ts.replace(microsecond=0),
            )
        }


class BaseSemverTest(TestCase, SnubaTestCase):
    __test__ = Abstract(__module__, __qualname__)

    KEY: str

    def setUp(self):
        super().setUp()
        self.ts = SnubaTagStorage()

    def run_test(self, query, expected_versions, environment=None, project=None):
        if project is None:
            project = self.project
        assert list(
            self.ts.get_tag_value_paginator(
                project.id,
                environment.id if environment else None,
                self.KEY,
                query=query,
            ).get_result(10)
        ) == [
            TagValue(
                key=self.KEY,
                value=v,
                times_seen=None,
                first_seen=None,
                last_seen=None,
            )
            for v in expected_versions
        ]


class GetTagValuePaginatorForProjectsSemverTest(BaseSemverTest):
    KEY = SEMVER_ALIAS

    def test_semver(self):
        env_2 = self.create_environment()
        project_2 = self.create_project()
        self.create_release(version="test@1.0.0.0+123", additional_projects=[project_2])
        self.create_release(version="test@1.2.3.4", environments=[self.environment, env_2])
        self.create_release(version="test@1.20.0.0-alpha", environments=[self.environment])
        self.create_release(version="test@1.20.3.0-beta+789", environments=[env_2])
        self.create_release(version="test@1.20.3.4", environments=[env_2])
        self.create_release(version="test2@2.0.0.0+456", environments=[self.environment, env_2])
        self.create_release(version="z_test@1.0.0.0")
        self.create_release(version="z_test@2.0.0.0+456", additional_projects=[project_2])
        # This shouldn't appear for any semver autocomplete
        self.create_release(version="test@abc123", additional_projects=[project_2])

        self.run_test(
            None,
            [
                "2.0.0.0",
                "1.20.3.4",
                "1.20.3.0-beta",
                "1.20.0.0-alpha",
                "1.2.3.4",
                "1.0.0.0",
            ],
        )
        self.run_test(
            "",
            [
                "2.0.0.0",
                "1.20.3.4",
                "1.20.3.0-beta",
                "1.20.0.0-alpha",
                "1.2.3.4",
                "1.0.0.0",
            ],
        )

        # These should all be equivalent
        self.run_test("1", ["1.20.3.4", "1.20.3.0-beta", "1.20.0.0-alpha", "1.2.3.4", "1.0.0.0"])
        self.run_test("1.", ["1.20.3.4", "1.20.3.0-beta", "1.20.0.0-alpha", "1.2.3.4", "1.0.0.0"])
        self.run_test("1.*", ["1.20.3.4", "1.20.3.0-beta", "1.20.0.0-alpha", "1.2.3.4", "1.0.0.0"])

        self.run_test("1.*", ["1.0.0.0"], project=project_2)

        self.run_test("1.2", ["1.20.3.4", "1.20.3.0-beta", "1.20.0.0-alpha", "1.2.3.4"])

        self.run_test("", ["2.0.0.0", "1.20.0.0-alpha", "1.2.3.4"], self.environment)
        self.run_test("", ["2.0.0.0", "1.20.3.4", "1.20.3.0-beta", "1.2.3.4"], env_2)
        self.run_test("1", ["1.20.0.0-alpha", "1.2.3.4"], self.environment)
        self.run_test("1", ["1.20.3.4", "1.20.3.0-beta", "1.2.3.4"], env_2)

        # Test packages handling

        self.run_test(
            "test",
            [
                "test2@2.0.0.0",
                "test@1.20.3.4",
                "test@1.20.3.0-beta",
                "test@1.20.0.0-alpha",
                "test@1.2.3.4",
                "test@1.0.0.0",
            ],
        )
        self.run_test("test", ["test@1.0.0.0"], project=project_2)
        self.run_test("test2", ["test2@2.0.0.0"])
        self.run_test("z", ["z_test@2.0.0.0", "z_test@1.0.0.0"])
        self.run_test("z", ["z_test@2.0.0.0"], project=project_2)

        self.run_test(
            "test@",
            [
                "test@1.20.3.4",
                "test@1.20.3.0-beta",
                "test@1.20.0.0-alpha",
                "test@1.2.3.4",
                "test@1.0.0.0",
            ],
        )
        self.run_test(
            "test@*",
            [
                "test@1.20.3.4",
                "test@1.20.3.0-beta",
                "test@1.20.0.0-alpha",
                "test@1.2.3.4",
                "test@1.0.0.0",
            ],
        )
        self.run_test(
            "test@1.2",
            ["test@1.20.3.4", "test@1.20.3.0-beta", "test@1.20.0.0-alpha", "test@1.2.3.4"],
        )


class GetTagValuePaginatorForProjectsSemverPackageTest(BaseSemverTest):
    KEY = SEMVER_PACKAGE_ALIAS

    def test_semver_package(self):
        env_2 = self.create_environment()
        project_2 = self.create_project()
        self.create_release(version="test@1.0.0.0+123", additional_projects=[project_2])
        self.create_release(version="test@1.2.0.0-alpha", environments=[self.environment])
        self.create_release(version="test2@2.0.0.0+456", environments=[self.environment, env_2])
        self.create_release(version="z_test@2.0.0.0+456", additional_projects=[project_2])
        # This shouldn't appear for any semver autocomplete
        self.create_release(version="test@abc123", additional_projects=[project_2])

        self.run_test(None, ["test", "test2", "z_test"])
        self.run_test("", ["test", "test2", "z_test"])

        self.run_test("t", ["test", "test2"])
        self.run_test("test", ["test", "test2"])
        self.run_test("test2", ["test2"])
        self.run_test("z", ["z_test"])

        self.run_test("", ["test", "z_test"], project=project_2)

        self.run_test("", ["test", "test2"], self.environment)
        self.run_test("", ["test2"], env_2)


class GetTagValuePaginatorForProjectsReleaseStageTest(TestCase, SnubaTestCase):
    def setUp(self):
        super().setUp()
        self.ts = SnubaTagStorage()

    def run_test(self, query, expected_releases, environment=None, project=None):
        if project is None:
            project = self.project
        assert list(
            self.ts.get_tag_value_paginator(
                project.id,
                environment.id if environment else None,
                RELEASE_STAGE_ALIAS,
                query=query,
            ).get_result(10)
        ) == [
            TagValue(
                key=RELEASE_STAGE_ALIAS,
                value=r.version,
                times_seen=None,
                first_seen=None,
                last_seen=None,
            )
            for r in expected_releases
        ]

    def test_release_stage(self):
        replaced_release = self.create_release(
            version="replaced_release",
            environments=[self.environment],
            adopted=timezone.now(),
            unadopted=timezone.now(),
        )
        adopted_release = self.create_release(
            version="adopted_release", environments=[self.environment], adopted=timezone.now()
        )
        not_adopted_release = self.create_release(
            version="not_adopted_release", environments=[self.environment]
        )

        env_2 = self.create_environment()
        project_2 = self.create_project()

        self.run_test(ReleaseStages.ADOPTED, [adopted_release], environment=self.environment)
        self.run_test(
            ReleaseStages.LOW_ADOPTION, [not_adopted_release], environment=self.environment
        )
        self.run_test(ReleaseStages.REPLACED, [replaced_release], environment=self.environment)

        self.run_test(ReleaseStages.ADOPTED, [], environment=env_2)
        self.run_test(ReleaseStages.ADOPTED, [], project=project_2, environment=self.environment)


class GetTagValuePaginatorForProjectsSemverBuildTest(BaseSemverTest):
    KEY = SEMVER_BUILD_ALIAS

    def test_semver_package(self):
        env_2 = self.create_environment()
        project_2 = self.create_project()
        self.create_release(version="test@1.0.0.0+123", additional_projects=[project_2])
        self.create_release(version="test@1.0.0.0+456")
        self.create_release(version="test@1.2.0.0", environments=[self.environment])
        self.create_release(version="test@1.2.1.0+124", environments=[self.environment])
        self.create_release(version="test@2.0.0.0+456", environments=[self.environment, env_2])
        self.create_release(version="test@2.0.1.0+457a", additional_projects=[project_2])
        self.create_release(version="test@2.0.1.0+789", additional_projects=[project_2])
        # This shouldn't appear for any semver autocomplete
        self.create_release(version="test@abc123", additional_projects=[project_2])

        self.run_test(None, ["123", "124", "456", "457a", "789"])
        self.run_test("", ["123", "124", "456", "457a", "789"])

        self.run_test("1", ["123", "124"])
        self.run_test("123", ["123"])
        self.run_test("4", ["456", "457a"])

        self.run_test("1", ["123"], project=project_2)
        self.run_test("1", ["124"], self.environment)
        self.run_test("4", ["456", "457a"])
        self.run_test("4", ["456"], env_2)
