from django.core import exceptions
from django.core.management import call_command
from django.shortcuts import get_object_or_404
from django.test import TestCase, override_settings
from io import StringIO

from hashid_field import Hashid, HashidField
from tests.forms import RecordForm, AlternateRecordForm
from tests.models import Record, Artist


class HashidsTests(TestCase):
    def setUp(self):
        self.record = Record.objects.create(name="Test Record", reference_id=123, key=456)
        self.hashids = self.record.reference_id.hashids

    def test_record_create(self):
        self.assertIsInstance(self.record, Record)

    def test_get_or_create(self):
        new_artist, created = Artist.objects.get_or_create(name="Get or Created Artist")
        self.assertIsInstance(new_artist, Artist)
        self.assertIsNotNone(new_artist.id)
        Record._meta.get_field('reference_id').allow_int_lookup = True
        new_record, created = Record.objects.get_or_create(name="Get or Created Record", reference_id=667373)
        self.assertIsInstance(new_record, Record)
        self.assertEqual(new_record.reference_id.id, 667373)
        Record._meta.get_field('reference_id').allow_int_lookup = False

    def test_record_reference_is_hashid(self):
        self.assertIsInstance(self.record.reference_id, Hashid)
        self.assertEqual(str(self.record.reference_id), self.hashids.encode(123))

    def test_record_load_from_db(self):
        record = Record.objects.get(pk=self.record.pk)
        self.assertIsInstance(record, Record)

    def test_record_reference_from_db_is_hashid(self):
        record = Record.objects.get(pk=self.record.pk)
        self.assertIsInstance(self.record.reference_id, Hashid)
        self.assertEqual(record.reference_id.hashid, self.hashids.encode(123))

    def test_set_int(self):
        self.record.reference_id = 456
        self.record.save()
        self.assertEqual(self.record.reference_id.hashid, self.hashids.encode(456))

    def test_set_hashid(self):
        self.record.reference_id = self.hashids.encode(789)
        self.record.save()
        self.assertEqual(str(self.record.reference_id), self.hashids.encode(789))

    def test_filter_by_int(self):
        # These should not return anything when integer lookups are not allowed
        self.assertFalse(Record.objects.filter(reference_id=123).exists())
        self.assertFalse(Record.objects.filter(reference_id__exact=123).exists())
        self.assertFalse(Record.objects.filter(reference_id__iexact=123).exists())
        self.assertFalse(Record.objects.filter(reference_id__contains=123).exists())
        self.assertFalse(Record.objects.filter(reference_id__icontains=123).exists())
        self.assertFalse(Record.objects.filter(reference_id__in=[123]).exists())

        # Tests when integer lookups are allowed
        Record._meta.get_field('reference_id').allow_int_lookup = True
        self.assertTrue(Record.objects.filter(reference_id=123).exists())
        self.assertTrue(Record.objects.filter(reference_id__exact=123).exists())
        self.assertTrue(Record.objects.filter(reference_id__iexact=123).exists())
        self.assertTrue(Record.objects.filter(reference_id__contains=123).exists())
        self.assertTrue(Record.objects.filter(reference_id__icontains=123).exists())
        self.assertTrue(Record.objects.filter(reference_id__in=[123]).exists())
        Record._meta.get_field('reference_id').allow_int_lookup = False

    def test_filter_by_string(self):
        self.assertTrue(Record.objects.filter(reference_id=str(self.record.reference_id)).exists())
        self.assertTrue(Record.objects.filter(reference_id__exact=str(self.record.reference_id)).exists())
        self.assertTrue(Record.objects.filter(reference_id__iexact=str(self.record.reference_id)).exists())
        self.assertTrue(Record.objects.filter(reference_id__contains=str(self.record.reference_id)).exists())
        self.assertTrue(Record.objects.filter(reference_id__icontains=str(self.record.reference_id)).exists())
        self.assertTrue(Record.objects.filter(reference_id__in=[str(self.record.reference_id)]).exists())

    def test_filter_by_hashid(self):
        self.assertTrue(Record.objects.filter(reference_id=self.hashids.encode(123)).exists())
        self.assertTrue(Record.objects.filter(reference_id__exact=self.hashids.encode(123)).exists())
        self.assertTrue(Record.objects.filter(reference_id__iexact=self.hashids.encode(123)).exists())
        self.assertTrue(Record.objects.filter(reference_id__contains=self.hashids.encode(123)).exists())
        self.assertTrue(Record.objects.filter(reference_id__icontains=self.hashids.encode(123)).exists())
        self.assertTrue(Record.objects.filter(reference_id__in=[self.hashids.encode(123)]).exists())

    def test_iterable_lookup(self):
        r1 = Record.objects.create(name="Red Album", reference_id=456)
        r2 = Record.objects.create(name="Blue Album", reference_id=789)
        # All 3 records exists (including record created in setUp())
        self.assertEqual(Record.objects.count(), 3)
        # Integers
        Record._meta.get_field('reference_id').allow_int_lookup = True
        self.assertEqual(Record.objects.filter(reference_id__in=[456, 789]).count(), 2)
        # Strings
        self.assertEqual(Record.objects.filter(reference_id__in=[456, str(r2.reference_id)]).count(), 2)
        self.assertEqual(Record.objects.filter(reference_id__in=[str(r1.reference_id), str(r2.reference_id)]).count(), 2)
        # Hashids
        self.assertEqual(Record.objects.filter(reference_id__in=[456, r2.reference_id]).count(), 2)
        self.assertEqual(Record.objects.filter(reference_id__in=[r1.reference_id, r2.reference_id]).count(), 2)
        # nonexistent integers
        self.assertEqual(Record.objects.filter(reference_id__in=[456, 1]).count(), 1)
        # nonexistent, but valid strings
        self.assertEqual(Record.objects.filter(reference_id__in=[456, self.hashids.encode(1)]).count(), 1)
        # nonexistent, but valid hashids
        self.assertEqual(Record.objects.filter(reference_id__in=[456, Record._meta.get_field('reference_id').encode_id(1)]).count(), 1)
        # Invalid integer
        self.assertFalse(Record.objects.filter(reference_id__in=[-1]).exists())
        # Invalid string
        self.assertFalse(Record.objects.filter(reference_id__in=["asdf"]).exists())
        Record._meta.get_field('reference_id').allow_int_lookup = False

    def test_subquery_lookup(self):
        a = Artist.objects.create(name="Artist A")
        b = Artist.objects.create(name="Artist B")
        c = Artist.objects.create(name="Artist C")
        queryset = Artist.objects.all()[:2]
        self.assertEqual(len(queryset), 2)
        self.assertEqual(len(Artist.objects.filter(id__in=queryset.values('id'))), 2)

    def test_passthrough_lookups(self):
        # Test null lookups
        self.assertTrue(Record.objects.filter(alternate_id__isnull=True).exists())
        self.assertFalse(Record.objects.filter(alternate_id__isnull=False).exists())

        # Create some new objects to test with
        a = Artist.objects.create(name="Artist A")
        b = Artist.objects.create(name="Artist B")
        c = Artist.objects.create(name="Artist C")
        r1 = self.record
        r2 = Record.objects.create(name="Red Album", reference_id=456)
        r3 = Record.objects.create(name="Blue Album", reference_id=789)

        # All 3 records exists (including record created in setUp())
        self.assertEqual(Record.objects.count(), 3)
        # greater than with hashid and integer values
        self.assertEqual(Artist.objects.filter(id__gt=a.id).count(), 2)
        self.assertEqual(Record.objects.filter(reference_id__gt=r1.reference_id).count(), 2)
        # great than or equal
        self.assertEqual(Artist.objects.filter(id__gte=a.id).count(), 3)
        self.assertEqual(Record.objects.filter(reference_id__gte=r1.reference_id.hashid).count(), 3)
        # less than
        self.assertEqual(Artist.objects.filter(id__lt=b.id).count(), 1)
        self.assertEqual(Record.objects.filter(reference_id__lt=r3.reference_id).count(), 2)
        # less than or equal
        self.assertEqual(Artist.objects.filter(id__lte=b.id.hashid).count(), 2)
        self.assertEqual(Record.objects.filter(reference_id__lte=r3.reference_id).count(), 3)
        # Make sure integer lookups are not allowed
        self.assertEqual(Artist.objects.filter(id__gt=a.id.id).count(), 0)
        self.assertEqual(Record.objects.filter(reference_id__gte=r1.reference_id.id).count(), 0)
        self.assertEqual(Artist.objects.filter(id__lt=999_999_999).count(), 0)
        self.assertEqual(Record.objects.filter(reference_id__lte=r3.reference_id.id).count(), 0)
        # Unless we turn them on
        Artist._meta.get_field('id').allow_int_lookup = True
        Record._meta.get_field('reference_id').allow_int_lookup = True
        self.assertEqual(Artist.objects.filter(id__gt=a.id.id).count(), 2)
        self.assertEqual(Record.objects.filter(reference_id__gte=r1.reference_id.id).count(), 3)
        self.assertEqual(Artist.objects.filter(id__lt=999_999_999).count(), 3)
        self.assertEqual(Record.objects.filter(reference_id__lte=r3.reference_id.id).count(), 3)
        Artist._meta.get_field('id').allow_int_lookup = False
        Record._meta.get_field('reference_id').allow_int_lookup = False

    def test_get_object_or_404(self):
        a = Artist.objects.create(name="Artist A")

        from django.http import Http404

        # Regular lookups should succeed
        self.assertEqual(get_object_or_404(Artist, pk=a.id), a)
        self.assertEqual(get_object_or_404(Artist, pk=str(a.id)), a)

        # Lookups for non-existant IDs should fail
        with self.assertRaises(Http404):
            get_object_or_404(Artist, pk=-1)
        with self.assertRaises(Http404):
            get_object_or_404(Artist, pk="asdf")

        # int lookups should fail
        with self.assertRaises(Http404):
            self.assertEqual(get_object_or_404(Artist, pk=int(a.id)), a)

    def test_invalid_int(self):
        with self.assertRaises(ValueError):
            self.record.reference_id = -5
            self.record.save()

    def test_invalid_string(self):
        with self.assertRaises(ValueError):
            self.record.reference_id = "asdfqwer"
            self.record.save()

    def test_custom_salt(self):
        r = Record.objects.create(reference_id=845, alternate_id=845)
        self.assertEqual(r.reference_id.id, r.alternate_id.id)
        self.assertNotEqual(r.reference_id, r.alternate_id)
        self.assertNotEqual(r.reference_id.hashid, r.alternate_id.hashid)

    def test_min_length(self):
        self.assertGreaterEqual(len(self.record.key), 10)

    def test_alphabet(self):
        for char in "efghijkpqrs4560":
            self.assertNotIn(char, self.record.key.hashid)

    def test_record_form(self):
        form = RecordForm(instance=self.record)
        self.assertEqual(form.initial['reference_id'].hashid, self.hashids.encode(123))
        form = RecordForm({'name': "A new name", 'reference_id': 987}, instance=self.record)
        self.assertTrue(form.is_valid())
        instance = form.save()
        self.assertEqual(self.record, instance)
        self.assertEqual(str(self.record.reference_id), self.hashids.encode(987))

    def test_invalid_id_in_form(self):
        form = RecordForm({'name': "A new name", 'reference_id': "asdfqwer"})
        self.assertFalse(form.is_valid())
        self.assertIn('reference_id', form.errors)

    def test_negative_int_in_form(self):
        form = RecordForm({'name': "A new name", 'reference_id': -5})
        self.assertFalse(form.is_valid())
        self.assertIn('reference_id', form.errors)

    def test_int_in_form(self):
        form = RecordForm({'name': "A new name", 'reference_id': 42})
        self.assertTrue(form.is_valid())

    def test_blank_for_nullable_field(self):
        name = "Blue Album"
        reference_id = 42
        form = AlternateRecordForm({'name': name, 'reference_id': reference_id})
        self.assertTrue(form.is_valid())
        instance = form.save()
        self.assertEqual(instance.name, name)
        self.assertEqual(instance.reference_id.id, reference_id)
        self.assertEqual(instance.alternate_id, '')
        instance.refresh_from_db()
        self.assertIsNone(instance.alternate_id)

    def test_autofield(self):
        a = Artist.objects.create(name="John Doe")
        b = Artist.objects.create(name="Jane Doe")
        self.assertIsInstance(a.id, Hashid)
        self.assertIsInstance(b.id, Hashid)
        self.assertListEqual(list(Artist.objects.order_by('id')), [a, b])

    def test_foreign_key(self):
        a = Artist.objects.create(name="John Doe")
        r = Record.objects.create(name="Blue Album", reference_id=456, artist=a)
        self.assertIsInstance(r, Record)
        self.assertIsInstance(r.artist, Artist)
        self.assertEqual(r.artist, a)
        self.assertTrue(Record.objects.filter(artist__id=a.id))

    def test_spanning_relationships(self):
        a = Artist.objects.create(name="John Doe")
        r = Record.objects.create(name="Blue Album", reference_id=456, artist=a)
        self.assertEqual(Record.objects.filter(artist__name="John Doe").first(), r)
        Record._meta.get_field('reference_id').allow_int_lookup = True
        self.assertEqual(Artist.objects.filter(records__reference_id=456).first(), a)
        Record._meta.get_field('reference_id').allow_int_lookup = False

    def test_dumpdata(self):
        a = Artist.objects.create(name="John Doe")
        r = Record.objects.create(name="Blue Album", reference_id=456, artist=a)
        out = StringIO()
        call_command("dumpdata", "tests.Artist", stdout=out)
        self.assertJSONEqual(out.getvalue(), '[{"pk": "bMrZ5lYd3axGxpW72Vo0", "fields": {"name": "John Doe"}, "model": "tests.artist"}]')
        out = StringIO()
        call_command("dumpdata", "tests.Record", stdout=out)
        self.assertJSONEqual(out.getvalue(), '[{"model": "tests.record", "pk": 1, "fields": {"name": "Test Record", "key": "82x1vxv21o", "alternate_id": null, "reference_id": "M3Ka6wW", "artist": null}}, {"model": "tests.record", "pk": 2, "fields": {"name": "Blue Album", "key": null, "alternate_id": null, "reference_id": "9wXZ03N", "artist": "bMrZ5lYd3axGxpW72Vo0"}}]')

    def test_loaddata(self):
        out = StringIO()
        call_command("loaddata", "artists", stdout=out)
        self.assertEqual(out.getvalue().strip(), "Installed 2 object(s) from 1 fixture(s)")
        self.assertEqual(Artist.objects.get(pk='bMrZ5lYd3axGxpW72Vo0').name, "John Doe")
        self.assertEqual(Artist.objects.get(pk="Ka0MzjgVGO031r5ybWkJ").name, "Jane Doe")

    @override_settings(HASHID_FIELD_LOOKUP_EXCEPTION=True)
    def test_exceptions(self):
        self.assertTrue(Record.objects.filter(key=str(self.record.key)).exists())
        self.assertTrue(Record.objects.filter(key__in=[str(self.record.key)]).exists())
        with self.assertRaises(ValueError):
            self.assertTrue(Record.objects.filter(key=456).exists())
        with self.assertRaises(ValueError):
            self.assertTrue(Record.objects.filter(key="asdf").exists())
        with self.assertRaises(ValueError):
            self.assertTrue(Record.objects.filter(key__in=[456]).exists())

    def test_custom_hashids_settings(self):
        SALT="abcd"
        ALPHABET="abcdefghijklmnop"
        MIN_LENGTH=10
        field = HashidField(salt=SALT, alphabet=ALPHABET, min_length=MIN_LENGTH)
        hashids = field._hashids
        self.assertEqual(hashids._salt, SALT)
        self.assertEqual(hashids._min_length, MIN_LENGTH)
        self.assertEqual("".join(sorted(hashids._alphabet + hashids._guards + hashids._separators)), ALPHABET)
        # Make sure all characters in 100 hashids are in the ALPHABET and are at least MIN_LENGTH
        for i in range(1, 100):
            hashid = str(field.to_python(i))
            self.assertGreaterEqual(len(hashid), MIN_LENGTH)
            for c in hashid:
                self.assertIn(c, ALPHABET)

    def test_invalid_alphabets(self):
        with self.assertRaises(exceptions.ImproperlyConfigured):
            HashidField(alphabet="")  # blank
        with self.assertRaises(exceptions.ImproperlyConfigured):
            HashidField(alphabet="abcdef")  # too short
        with self.assertRaises(exceptions.ImproperlyConfigured):
            HashidField(alphabet="abcdefghijklmno")  # too short by one
        with self.assertRaises(exceptions.ImproperlyConfigured):
            HashidField(alphabet="aaaaaaaaaaaaaaaaaaaaa")  # not unique
        with self.assertRaises(exceptions.ImproperlyConfigured):
            HashidField(alphabet="aabcdefghijklmno")  # not unique by one