#
#   Copyright (c) 2014-2015, 2018 eGauge Systems LLC
# 	1644 Conestoga St, Suite 2
# 	Boulder, CO 80301
# 	voice: 720-545-9767
# 	email: davidm@egauge.net
#
#   All rights reserved.
#
#   This code is the property of eGauge Systems LLC and may not be
#   copied, modified, or disclosed without any prior and written
#   permission from eGauge Systems LLC.
#
'''Unit tests.  Run, e.g., from epic-sample with:

	python3 manage.py test epic.tests
'''
import io
import os
import re

from datetime import timedelta

from django.test import TestCase
from django.utils import timezone
from django.contrib.auth import get_user_model
from django.contrib.auth import models as auth_models

import epic
from epic import perms
from epic.compat import reverse
from epic.models import Assembly_Item, Delta, Inventory, Line_Item, Order, \
    Part, Shipment, Vendor, Vendor_Part, Warehouse
from epic import urls

class URL_Iterator:
    def __init__(self, urlpatterns):
        self.urlpattern_iterator = urlpatterns.__iter__()

    def __iter__(self):
        return self

    def __next__(self):
        # pylint: disable=protected-access
        try:
            pat = self.urlpattern_iterator.__next__() # Python 3
        except AttributeError:
            pat = self.urlpattern_iterator.next() # Python 2
        regex = re.compile(pat.pattern._regex)
        args = {}
        for key in regex.groupindex.keys():
            args[key] = 1
        url = reverse('epic:%s' % pat.name, kwargs=args)
        return url, pat.name

    def next(self):
        """For Python 2 compatibility."""
        return self.__next__()

def create_part(pk):
    return Part(pk=pk, mfg='mfg', mfg_pn='mfg_pn%d' % pk,
                mounting=Part.MOUNTING_SMD, target_price=1, overage=0,
                spq=1, status=Part.STATUS_ACTIVE, lead_time=9)

def create_warehouse(pk, name):
    return Warehouse(pk=pk, name=name)

def create_vendor(pk, name):
    return Vendor(pk=pk, name=name)

def create_vendor_part(part_id, vendor_id, vendor_pn, price):
    return Vendor_Part(part_id=part_id, vendor_id=vendor_id,
                       vendor_pn=vendor_pn, price=price)

def create_order(pk, vendor_id, warehouse_id):
    now = timezone.now()
    then = now + timedelta(days=30)
    return Order(pk=pk, ts=now, warehouse_id=warehouse_id,
                 expected_arrival_date=then,
                 status=Order.STATUS_OPEN, vendor_id=vendor_id)

def create_ship(pk, order_id, warehouse_id, tracking,
                cost_freight, cost_other, cost_discount):
    return Shipment(pk=pk, ts=timezone.now(), tracking=tracking,
                    ordr_id=order_id, warehouse_id=warehouse_id,
                    cost_freight=cost_freight, cost_other=cost_other,
                    cost_discount=cost_discount)

def create_inventory(pk, warehouse_id):
    return Inventory(pk=pk, ts=timezone.now(), warehouse_id=warehouse_id)

def create_assembly_item(assy_id=None, comp_id=None, qty=1):
    return Assembly_Item(assy_id=assy_id, comp_id=comp_id, qty=qty)

def create_line_item(txtn_id, part_id, qty, line_cost, index):
    return Line_Item(txtn_id=txtn_id, part_id=part_id, qty=qty,
                     line_cost=line_cost, index=index)

class Part_Method_Tests(TestCase):

    def test_is_orderable(self):
        p = Part(status=Part.STATUS_PREVIEW)
        self.assertEqual(p.is_orderable(), False)

        p = Part(status=Part.STATUS_ACTIVE)
        self.assertEqual(p.is_orderable(), True)

        p = Part(status=Part.STATUS_DEPRECATED)
        self.assertEqual(p.is_orderable(), True)

        p = Part(status=Part.STATUS_OBSOLETE)
        self.assertEqual(p.is_orderable(), False)

    def test_equivalent_parts(self):
        p1 = create_part(1)
        self.assertEqual(p1.equivalent_parts(), set([p1]))

        p2 = create_part(2)

        p1.substitute = p2
        p1.save()
        p2.save()
        self.assertEqual(p1.equivalent_parts(), set([p1, p2]))
        self.assertEqual(p2.equivalent_parts(), set([p1, p2]))

        p3 = create_part(3)
        p3.substitute = p2
        p3.save()
        self.assertEqual(p1.equivalent_parts(), set([p1, p2, p3]))
        self.assertEqual(p2.equivalent_parts(), set([p1, p2, p3]))
        self.assertEqual(p3.equivalent_parts(), set([p1, p2, p3]))

        p1.substitute = None
        p1.save()
        self.assertEqual(p1.equivalent_parts(), set([p1]))
        self.assertEqual(p2.equivalent_parts(), set([p2, p3]))
        self.assertEqual(p3.equivalent_parts(), set([p2, p3]))

        p1.delete()
        p2.delete()
        p3.delete()

class Access_Tests(TestCase):
    @classmethod
    def setup_part(cls):
        p1 = create_part(1)
        p1.save()
        ai = create_assembly_item(assy_id=1, comp_id=2, qty=3)
        ai.refdes = 'R1,R2,R3'
        ai.save()
        p = create_vendor_part(1, 2, 'ASSY_HOUSE_VENDOR_PART_#1', 325.654321)
        p.save()

    @classmethod
    def setup_vendor(cls):
        v = create_vendor(1, 'Vendor #1')
        v.save()
        p = create_vendor_part(1, 1, 'VENDOR_PART_#1', 535.123456)
        p.save()

    @classmethod
    def setup_warehouse(cls):
        w = create_warehouse(1, 'Warehouse #1')
        w.save()

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        perms.add_custom_permissions(auth_models)
        u = get_user_model().objects.create_user('test', 'test@magic.com',
                                                 'magic')
        u.user_permissions.set([perms.VIEW_PERM, perms.EDIT_PERM])

        p2 = create_part(2)
        p2.save()

        p3 = create_part(3)
        p3.save()

        w = create_warehouse(2, 'Assyhouse #1')
        w.save()
        v = create_vendor(2, 'Assyhouse #1')
        v.save()

        Access_Tests.setup_part()
        Access_Tests.setup_vendor()
        Access_Tests.setup_warehouse()

    # def test_redirect_to_https(self):
    #     url = reverse('epic:epic_index')
    #     response = self.client.get(url, secure=False)
    #     self.assertEqual(response.status_code, 301)
    #     self.assertIsNotNone(re.match('https://', response.url))

    def test_unauthenticated_accesses(self):
        response = self.client.get(reverse('epic:part_info') + '?pid=1',
                                   secure=True)
        self.assertEqual(response.status_code, 302)

        for url, _ in URL_Iterator(urls.urlpatterns):
            m = re.match(r'/epic/dal/', url)
            # auto-completion URLs respond with 403 (Forbidden)
            # others return redirect to login page:
            if m:
                expected = 403
            else:
                expected = 302
            response = self.client.get(url, secure=True)
            if response.status_code != expected:
                print('Unauthenticated access to %s returned %s, expected %s' %
                      (url, response.status_code, expected))
            self.assertEqual(response.status_code, expected)

    def test_authenticated_accesses(self):
        self.client.login(username='test', password='magic')

        for url, pattern_name in URL_Iterator(urls.urlpatterns):
            if re.match('order_', pattern_name):
                # order part 1 (an assembly) from vendor 'Assyhouse #1'
                o = create_order(1, 2, 1)
                o.save()
                l = create_line_item(1, 1, qty=13, line_cost='15.95', index=1)
                l.save()
            elif re.match('ship_', pattern_name):
                # order part 1(an assembly) from vendor 'Assyhouse #1'
                o = create_order(2, 2, 1)
                o.save()
                l = create_line_item(2, 1, qty=13, line_cost='15.95', index=1)
                l.save()
                s = create_ship(1, 2, 1, '1234,5678', 1.1, 2.2, 3.3)
                s.save()
                l = create_line_item(1, 1, qty=2, line_cost='2', index=1)
                l.save()
            elif re.match('warehouse_inventory_', pattern_name):
                i = create_inventory(1, 1)
                i.save()

            expect = 200
            expect_url = None
            post_action = []
            if pattern_name == 'search_results':
                url += '?q=1'
                # with a unique result, search redirects to the detail page:
                expect = 302
                expect_url = reverse('epic:part_detail', kwargs={'pk': 1})
            elif pattern_name == 'part_delete':
                expect = 302
                expect_url = reverse('epic:part_list')
                post_action.append(Access_Tests.setup_part)
            elif pattern_name == 'ship_delete':
                expect = 302
                expect_url = reverse('epic:ship_list')
            elif pattern_name == 'vendor_delete':
                expect = 302
                expect_url = reverse('epic:vendor_list')
                post_action.append(Access_Tests.setup_vendor)
            elif pattern_name == 'warehouse_delete':
                expect = 302
                expect_url = reverse('epic:warehouse_list')
                post_action.append(Access_Tests.setup_warehouse)
            elif pattern_name == 'warehouse_inventory_delete':
                expect = 302
                expect_url = reverse('epic:warehouse_detail', kwargs={'pk':1})
            elif pattern_name == 'order_delete':
                expect = 302
                expect_url = reverse('epic:order_list')
            elif pattern_name in ['datasheet_add_part', 'datasheet_detail',
                                  'datasheet_edit', 'datasheet_delete']:
                continue

            response = self.client.get(url, secure=True)

            # we can't only have one transaction with pk=1...
            Shipment.objects.all().delete()
            Order.objects.all().delete()
            Inventory.objects.all().delete()

            if response.status_code != expect:
                print('access to %s failed' % url)
                if hasattr(response, 'url'):
                    print(' redirect URL: %s' % response.url)
            self.assertEqual(response.status_code, expect)
            if expect_url is not None:
                url = response.url
                if url[-1] == '/':
                    url = url[:-1]
                self.assertEqual(url, expect_url)

            for action in post_action:
                action()

    def test_form_upload(self):
        self.client.login(username='test', password='magic')

        # Verify that we can post item formset to an order:
        response = self.client.post('/epic/order/add',
                                    {
                                        'ts': '2018-08-06',
                                        'vendor': 1,
                                        'warehouse': 2,
                                        'expected_arrival_date': '2018-08-06',
                                        'notes': 'This is a note',
                                        'line_item_set-TOTAL_FORMS': 12,
                                        'line_item_set-INITIAL_FORMS': 0,
                                        'line_item_set-MIN_NUM_FORMS': 0,
                                        'line_item_set-MAX_NUM_FORMS': 1000,
                                        'line_item_set-0-qty': 8,
                                        'line_item_set-0-part': 3,
                                        'line_item_set-0-vendor_pn': 'VPN #1',
                                        'line_item_set-0-part_cost': 1,
                                        'line_item_set-0-line_cost': 120,
                                        'line_item_set-1-qty': 18,
                                        'line_item_set-1-part': 2,
                                        'line_item_set-1-vendor_pn': 'VPN #2',
                                        'line_item_set-1-part_cost': 1,
                                        'line_item_set-1-line_cost': 121
                                    })

        # response.request['wsgi.input']._FakePayload__content.seek(0)
        # print('REQUEST',
        #       response.request['wsgi.input']._FakePayload__content.read()
        #       .decode('utf-8'))
        if response.status_code != 302:
            for name in response.context.keys():
                if re.match(r'[^_]+_form$', name):
                    print('%s.errors:' % name, response.context[name].errors)

        self.assertEqual(response.status_code, 302)
        order = Order.objects.all()[0]
        line_items = Line_Item.objects.filter(txtn=order)
        self.assertEqual(line_items[0].qty, 8)
        self.assertEqual(line_items[0].part.id, 3)
        self.assertEqual(line_items[0].line_cost, 120)
        self.assertEqual(line_items[1].qty, 18)
        self.assertEqual(line_items[1].part.id, 2)
        self.assertEqual(line_items[1].line_cost, 121)

        # Verify that we can post an item formset to a shipment:
        response = self.client.post('/epic/order/%d/add-shipment' % order.id,
                                    {
                                        'ts': '2018-08-06',
                                        'cost_freight': 10.0,
                                        'cost_other': 3.14,
                                        'cost_discount': 2.74,
                                        'vendor': 1,
                                        'warehouse': 2,
                                        'expected_arrival_date': '2018-08-06',
                                        'notes': 'note 2',
                                        'line_item_set-TOTAL_FORMS': 12,
                                        'line_item_set-INITIAL_FORMS': 0,
                                        'line_item_set-MIN_NUM_FORMS': 0,
                                        'line_item_set-MAX_NUM_FORMS': 1000,
                                        'line_item_set-0-qty': 7,
                                        'line_item_set-0-part': 3,
                                        'line_item_set-0-vendor_pn': 'VPN #1',
                                        'line_item_set-0-part_cost': 1,
                                        'line_item_set-0-line_cost': 120,
                                        'line_item_set-1-qty': 17,
                                        'line_item_set-1-part': 2,
                                        'line_item_set-1-vendor_pn': 'VPN #2',
                                        'line_item_set-1-part_cost': 1,
                                        'line_item_set-1-line_cost': 121
                                    })
        self.assertEqual(response.status_code, 302)
        ship = Shipment.objects.all()[0]
        line_items = Line_Item.objects.filter(txtn=ship)
        self.assertEqual(line_items[0].qty, 7)
        self.assertEqual(line_items[0].part.id, 3)
        self.assertEqual(line_items[0].line_cost, 120)
        self.assertEqual(line_items[1].qty, 17)
        self.assertEqual(line_items[1].part.id, 2)
        self.assertEqual(line_items[1].line_cost, 121)

        # Verify that we can post a delta formset as an inventory:
        response = self.client.post('/epic/warehouse/1/add-inventory',
                                    {
                                        'ts': '2018-08-06 08:15 AM',
                                        'notes': 'Note #3',
                                        'delta_set-TOTAL_FORMS': 12,
                                        'delta_set-INITIAL_FORMS': 0,
                                        'delta_set-MIN_NUM_FORMS': 0,
                                        'delta_set-MAX_NUM_FORMS': 1000,
                                        'delta_set-0-adj': 6,
                                        'delta_set-0-part': 2,
                                        'delta_set-0-vendor_pn': 'VPN #2',
                                        'delta_set-0-part_cost': 1,
                                        'delta_set-0-line_cost': 120,
                                        'delta_set-1-adj': 16,
                                        'delta_set-1-part': 3,
                                        'delta_set-1-vendor_pn': 'VPN #3',
                                        'delta_set-1-part_cost': 1,
                                        'delta_set-1-line_cost': 121
                                    })
        self.assertEqual(response.status_code, 302)
        inv = Inventory.objects.all()[0]
        line_items = Delta.objects.filter(txtn=inv)
        self.assertEqual(line_items[0].part.id, 2)
        self.assertEqual(line_items[0].is_absolute, True)
        self.assertEqual(line_items[0].adj, 6)
        self.assertEqual(line_items[0].warehouse.id, 1)
        self.assertEqual(line_items[1].part.id, 3)
        self.assertEqual(line_items[1].is_absolute, True)
        self.assertEqual(line_items[1].adj, 16)
        self.assertEqual(line_items[1].warehouse.id, 1)

    def test_spreadsheet_upload(self):
        self.client.login(username='test', password='magic')
        # import pdb
        # pdb.set_trace()

        # Verify invalid column names yield the expected errors:
        response = self.client.post('/epic/order/add',
                                    {
                                        'ts': '2018-08-06',
                                        'vendor': 1,
                                        'warehouse': 2,
                                        'expected_arrival_date': '2018-08-06',
                                        'notes': 'This is a note',
                                        'spreadsheet': io.BytesIO(
                                            b'qty1,part1,extra,amount1\n'
                                            b'10,3,5,99\n'
                                            b'40,EP0002,,10\n'),
                                        'line_item_set-TOTAL_FORMS': 0,
                                        'line_item_set-INITIAL_FORMS': 0
                                    })
        self.assertFormError(response, 'order_form', 'spreadsheet',
                             ['Column qty missing.  It may also be called '
                              'one of: quantity, count.',
                              'Column part missing.  It may also be called '
                              'one of: pn, ep.',
                              'Column amount missing.  It may also be called '
                              'one of: cost.'])

        # Verify that we can post a CSV file to an order:
        response = self.client.post('/epic/order/add',
                                    {
                                        'ts': '2018-08-06',
                                        'vendor': 1,
                                        'warehouse': 2,
                                        'expected_arrival_date': '2018-08-06',
                                        'notes': 'This is a note',
                                        'spreadsheet': io.BytesIO(
                                            b'qty,part,extra,amount\n'
                                            b'10,3,5,99\n'
                                            b'40,EP0002,,10\n'),
                                        'line_item_set-TOTAL_FORMS': 0,
                                        'line_item_set-INITIAL_FORMS': 0
                                    })
        self.assertEqual(response.status_code, 302)
        order = Order.objects.all()[0]
        line_items = Line_Item.objects.filter(txtn=order)
        self.assertEqual(line_items[0].qty, 10)
        self.assertEqual(line_items[0].part.id, 3)
        self.assertEqual(line_items[0].line_cost, 99)
        self.assertEqual(line_items[1].qty, 40)
        self.assertEqual(line_items[1].part.id, 2)
        self.assertEqual(line_items[1].line_cost, 10)

        # Verify that we can post a CSV file to a shipment:
        response = self.client.post('/epic/order/%d/add-shipment' % order.id,
                                    {
                                        'ts': '2018-08-06',
                                        'cost_freight': 10.0,
                                        'cost_other': 3.14,
                                        'cost_discount': 2.74,
                                        'vendor': 1,
                                        'warehouse': 2,
                                        'expected_arrival_date': '2018-08-06',
                                        'notes': 'note 2',
                                        'spreadsheet': io.BytesIO(
                                            b'qty,part,extra,amount\n'
                                            b'5,3,5,99\n'
                                            b'10,EP0002,,10\n')
                                    })
        self.assertEqual(response.status_code, 302)
        ship = Shipment.objects.all()[0]
        line_items = Line_Item.objects.filter(txtn=ship)
        self.assertEqual(line_items[0].qty, 5)
        self.assertEqual(line_items[0].part.id, 3)
        self.assertEqual(line_items[0].line_cost, 99)
        self.assertEqual(line_items[1].qty, 10)
        self.assertEqual(line_items[1].part.id, 2)
        self.assertEqual(line_items[1].line_cost, 10)

        # Verify that we can post an Excel file as an inventory:
        excel_file = os.path.join(os.path.dirname(epic.__file__),
                                  'test-data', 'inventory.xlsx')
        with open(excel_file, 'rb') as f:
            response = self.client.post('/epic/warehouse/1/add-inventory',
                                        {
                                            'ts': '2018-08-06 08:15 AM',
                                            'spreadsheet': f
                                        })
        self.assertEqual(response.status_code, 302)
        inv = Inventory.objects.all()[0]
        line_items = Delta.objects.filter(txtn=inv)
        self.assertEqual(line_items[0].part.id, 2)
        self.assertEqual(line_items[0].is_absolute, True)
        self.assertEqual(line_items[0].adj, 1234)
        self.assertEqual(line_items[0].warehouse.id, 1)
        self.assertEqual(line_items[1].part.id, 3)
        self.assertEqual(line_items[1].is_absolute, True)
        self.assertEqual(line_items[1].adj, 5678)
        self.assertEqual(line_items[1].warehouse.id, 1)

    def test_order_status(self):
        '''Confirm that deleting a shipment for completed order will mark
        order as OPEN again.

        '''
        self.client.login(username='test', password='magic')
        # create order from Vendor #1:
        o = create_order(pk=4, vendor_id=1, warehouse_id=1)
        o.save()
        l = create_line_item(o.pk, 2, qty=1000, line_cost='256.35', index=1)
        l.save()

        # order should be open now
        self.assertEqual(o.status, Order.STATUS_OPEN)

        # create two shipments that should complete the order:
        s = create_ship(2, o.pk, 1, '1Z1345', 1.23, 2.34, 3.45)
        s.save()
        l = create_line_item(s.pk, 2, 900, line_cost='230.05', index=1)
        l.save()

        s = create_ship(3, o.pk, 1, '1Z1356', 1.23, 2.34, 3.45)
        s.save()
        l = create_line_item(s.pk, 2, 100, line_cost='230.05', index=1)
        l.save()

        s.finalize()

        # order should be closed now:
        o = Order.objects.get(pk=4)
        self.assertEqual(o.status, Order.STATUS_CLOSED)

        # delete the first shipment:
        url = reverse('epic:ship_delete', kwargs={'pk':2})
        response = self.client.get(url, secure=True)
        self.assertEqual(response.status_code, 302)

        # order should be open again:
        o = Order.objects.get(pk=4)
        self.assertEqual(o.status, Order.STATUS_OPEN)
