"""Unit testing for the various report models"""

import os
import shutil
from pathlib import Path

from django.conf import settings
from django.core.cache import cache
from django.http.response import StreamingHttpResponse
from django.test import TestCase
from django.urls import reverse
from django.utils.safestring import SafeString

from PIL import Image

import report.models as report_models
from build.models import Build
from common.models import InvenTreeSetting, InvenTreeUserSetting
from InvenTree.unit_test import InvenTreeAPITestCase
from report.templatetags import barcode as barcode_tags
from report.templatetags import report as report_tags
from stock.models import StockItem, StockItemAttachment


class ReportTagTest(TestCase):
    """Unit tests for the report template tags"""

    def debug_mode(self, value: bool):
        """Enable or disable debug mode for reports"""
        InvenTreeSetting.set_setting('REPORT_DEBUG_MODE', value, change_user=None)

    def test_getindex(self):
        """Tests for the 'getindex' template tag"""

        fn = report_tags.getindex
        data = [1, 2, 3, 4, 5, 6]

        # Out of bounds or invalid
        self.assertEqual(fn(data, -1), None)
        self.assertEqual(fn(data, 99), None)
        self.assertEqual(fn(data, 'xx'), None)

        for idx in range(len(data)):
            self.assertEqual(fn(data, idx), data[idx])

    def test_getkey(self):
        """Tests for the 'getkey' template tag"""

        data = {
            'hello': 'world',
            'foo': 'bar',
            'with spaces': 'withoutspaces',
            1: 2,
        }

        for k, v in data.items():
            self.assertEqual(report_tags.getkey(data, k), v)

    def test_asset(self):
        """Tests for asset files"""

        # Test that an error is raised if the file does not exist
        for b in [True, False]:
            self.debug_mode(b)

            with self.assertRaises(FileNotFoundError):
                report_tags.asset("bad_file.txt")

        # Create an asset file
        asset_dir = settings.MEDIA_ROOT.joinpath('report', 'assets')
        asset_dir.mkdir(parents=True, exist_ok=True)
        asset_path = asset_dir.joinpath('test.txt')

        asset_path.write_text("dummy data")

        self.debug_mode(True)
        asset = report_tags.asset('test.txt')
        self.assertEqual(asset, '/media/report/assets/test.txt')

        # Ensure that a 'safe string' also works
        asset = report_tags.asset(SafeString('test.txt'))
        self.assertEqual(asset, '/media/report/assets/test.txt')

        self.debug_mode(False)
        asset = report_tags.asset('test.txt')
        self.assertEqual(asset, f'file://{asset_dir}/test.txt')

    def test_uploaded_image(self):
        """Tests for retrieving uploaded images"""

        # Test for a missing image
        for b in [True, False]:
            self.debug_mode(b)

            with self.assertRaises(FileNotFoundError):
                report_tags.uploaded_image('/part/something/test.png', replace_missing=False)

            img = report_tags.uploaded_image('/part/something/other.png')
            self.assertTrue('blank_image.png' in img)

        # Create a dummy image
        img_path = 'part/images/'
        img_path = settings.MEDIA_ROOT.joinpath(img_path)
        img_file = img_path.joinpath('test.jpg')

        img_path.mkdir(parents=True, exist_ok=True)
        img_file.write_text("dummy data")

        # Test in debug mode. Returns blank image as dummy file is not a valid image
        self.debug_mode(True)
        img = report_tags.uploaded_image('part/images/test.jpg')
        self.assertEqual(img, '/static/img/blank_image.png')

        # Now, let's create a proper image
        img = Image.new('RGB', (128, 128), color='RED')
        img.save(img_file)

        # Try again
        img = report_tags.uploaded_image('part/images/test.jpg')
        self.assertEqual(img, '/media/part/images/test.jpg')

        # Ensure that a 'safe string' also works
        img = report_tags.uploaded_image(SafeString('part/images/test.jpg'))
        self.assertEqual(img, '/media/part/images/test.jpg')

        self.debug_mode(False)
        img = report_tags.uploaded_image('part/images/test.jpg')
        self.assertEqual(img, f'file://{img_path.joinpath("test.jpg")}')

        img = report_tags.uploaded_image(SafeString('part/images/test.jpg'))
        self.assertEqual(img, f'file://{img_path.joinpath("test.jpg")}')

    def test_part_image(self):
        """Unit tests for the 'part_image' tag"""

        with self.assertRaises(TypeError):
            report_tags.part_image(None)

    def test_company_image(self):
        """Unit tests for the 'company_image' tag"""

        with self.assertRaises(TypeError):
            report_tags.company_image(None)

    def test_logo_image(self):
        """Unit tests for the 'logo_image' tag"""

        # By default, should return the core InvenTree logo
        for b in [True, False]:
            self.debug_mode(b)
            logo = report_tags.logo_image()
            self.assertIn('inventree.png', logo)

    def test_maths_tags(self):
        """Simple tests for mathematical operator tags"""

        self.assertEqual(report_tags.add(1, 2), 3)
        self.assertEqual(report_tags.subtract(10, 4.2), 5.8)
        self.assertEqual(report_tags.multiply(2.3, 4), 9.2)
        self.assertEqual(report_tags.divide(100, 5), 20)


class BarcodeTagTest(TestCase):
    """Unit tests for the barcode template tags"""

    def test_barcode(self):
        """Test the barcode generation tag"""

        barcode = barcode_tags.barcode("12345")

        self.assertTrue(isinstance(barcode, str))
        self.assertTrue(barcode.startswith('data:image/png;'))

        # Try with a different format
        barcode = barcode_tags.barcode('99999', format='BMP')
        self.assertTrue(isinstance(barcode, str))
        self.assertTrue(barcode.startswith('data:image/bmp;'))

    def test_qrcode(self):
        """Test the qrcode generation tag"""

        # Test with default settings
        qrcode = barcode_tags.qrcode("hello world")
        self.assertTrue(isinstance(qrcode, str))
        self.assertTrue(qrcode.startswith('data:image/png;'))
        self.assertEqual(len(qrcode), 700)

        # Generate a much larger qrcode
        qrcode = barcode_tags.qrcode(
            "hello_world",
            version=2,
            box_size=50,
            format='BMP',
        )
        self.assertTrue(isinstance(qrcode, str))
        self.assertTrue(qrcode.startswith('data:image/bmp;'))
        self.assertEqual(len(qrcode), 309720)


class ReportTest(InvenTreeAPITestCase):
    """Base class for unit testing reporting models"""
    fixtures = [
        'category',
        'part',
        'company',
        'location',
        'supplier_part',
        'stock',
        'stock_tests',
        'bom',
        'build',
    ]

    model = None
    list_url = None
    detail_url = None
    print_url = None

    def setUp(self):
        """Ensure cache is cleared as part of test setup"""
        cache.clear()
        return super().setUp()

    def copyReportTemplate(self, filename, description):
        """Copy the provided report template into the required media directory."""
        src_dir = Path(__file__).parent.joinpath(
            'templates',
            'report'
        )

        template_dir = os.path.join(
            'report',
            'inventree',
            self.model.getSubdir(),
        )

        dst_dir = settings.MEDIA_ROOT.joinpath(template_dir)

        if not dst_dir.exists():  # pragma: no cover
            dst_dir.mkdir(parents=True, exist_ok=True)

        src_file = src_dir.joinpath(filename)
        dst_file = dst_dir.joinpath(filename)

        if not dst_file.exists():  # pragma: no cover
            shutil.copyfile(src_file, dst_file)

        # Convert to an "internal" filename
        db_filename = os.path.join(
            template_dir,
            filename
        )

        # Create a database entry for this report template!
        self.model.objects.create(
            name=os.path.splitext(filename)[0],
            description=description,
            template=db_filename,
            enabled=True
        )

    def test_list_endpoint(self):
        """Test that the LIST endpoint works for each report."""
        if not self.list_url:
            return

        url = reverse(self.list_url)

        response = self.get(url)
        self.assertEqual(response.status_code, 200)

        reports = self.model.objects.all()

        n = len(reports)
        # API endpoint must return correct number of reports
        self.assertEqual(len(response.data), n)

        # Filter by "enabled" status
        response = self.get(url, {'enabled': True})
        self.assertEqual(len(response.data), n)

        response = self.get(url, {'enabled': False})
        self.assertEqual(len(response.data), 0)

        # Disable each report
        for report in reports:
            report.enabled = False
            report.save()

        # Filter by "enabled" status
        response = self.get(url, {'enabled': True})
        self.assertEqual(len(response.data), 0)

        response = self.get(url, {'enabled': False})
        self.assertEqual(len(response.data), n)

    def test_metadata(self):
        """Unit tests for the metadata field."""
        if self.model is not None:
            p = self.model.objects.first()

            self.assertEqual(p.metadata, {})

            self.assertIsNone(p.get_metadata('test'))
            self.assertEqual(p.get_metadata('test', backup_value=123), 123)

            # Test update via the set_metadata() method
            p.set_metadata('test', 3)
            self.assertEqual(p.get_metadata('test'), 3)

            for k in ['apple', 'banana', 'carrot', 'carrot', 'banana']:
                p.set_metadata(k, k)

            self.assertEqual(len(p.metadata.keys()), 4)


class TestReportTest(ReportTest):
    """Unit testing class for the stock item TestReport model"""
    model = report_models.TestReport

    list_url = 'api-stockitem-testreport-list'
    detail_url = 'api-stockitem-testreport-detail'
    print_url = 'api-stockitem-testreport-print'

    def setUp(self):
        """Setup function for the stock item TestReport"""
        self.copyReportTemplate('inventree_test_report.html', 'stock item test report')

        return super().setUp()

    def test_print(self):
        """Printing tests for the TestReport."""
        report = self.model.objects.first()

        url = reverse(self.print_url, kwargs={'pk': report.pk})

        # Try to print without providing a valid StockItem
        response = self.get(url, expected_code=400)

        # Try to print with an invalid StockItem
        response = self.get(url, {'item': 9999}, expected_code=400)

        # Now print with a valid StockItem
        item = StockItem.objects.first()

        response = self.get(url, {'item': item.pk}, expected_code=200)

        # Response should be a StreamingHttpResponse (PDF file)
        self.assertEqual(type(response), StreamingHttpResponse)

        headers = response.headers
        self.assertEqual(headers['Content-Type'], 'application/pdf')

        # By default, this should *not* have created an attachment against this stockitem
        self.assertFalse(StockItemAttachment.objects.filter(stock_item=item).exists())

        # Change the setting, now the test report should be attached automatically
        InvenTreeSetting.set_setting('REPORT_ATTACH_TEST_REPORT', True, None)

        response = self.get(url, {'item': item.pk}, expected_code=200)

        headers = response.headers
        self.assertEqual(headers['Content-Type'], 'application/pdf')

        # Check that a report has been uploaded
        attachment = StockItemAttachment.objects.filter(stock_item=item).first()
        self.assertIsNotNone(attachment)


class BuildReportTest(ReportTest):
    """Unit test class for the BuildReport model"""
    model = report_models.BuildReport

    list_url = 'api-build-report-list'
    detail_url = 'api-build-report-detail'
    print_url = 'api-build-report-print'

    def setUp(self):
        """Setup unit testing functions"""
        self.copyReportTemplate('inventree_build_order.html', 'build order template')

        return super().setUp()

    def test_print(self):
        """Printing tests for the BuildReport."""
        report = self.model.objects.first()

        url = reverse(self.print_url, kwargs={'pk': report.pk})

        # Try to print without providing a valid BuildOrder
        response = self.get(url, expected_code=400)

        # Try to print with an invalid BuildOrder
        response = self.get(url, {'build': 9999}, expected_code=400)

        # Now print with a valid BuildOrder

        build = Build.objects.first()

        response = self.get(url, {'build': build.pk})

        self.assertEqual(type(response), StreamingHttpResponse)

        headers = response.headers

        self.assertEqual(headers['Content-Type'], 'application/pdf')
        self.assertEqual(headers['Content-Disposition'], 'attachment; filename="report.pdf"')

        # Now, set the download type to be "inline"
        inline = InvenTreeUserSetting.get_setting_object('REPORT_INLINE', cache=False, user=self.user)
        inline.value = True
        inline.save()

        response = self.get(url, {'build': 1})
        headers = response.headers
        self.assertEqual(headers['Content-Type'], 'application/pdf')
        self.assertEqual(headers['Content-Disposition'], 'inline; filename="report.pdf"')


class BOMReportTest(ReportTest):
    """Unit test class for the BillOfMaterialsReport model"""
    model = report_models.BillOfMaterialsReport

    list_url = 'api-bom-report-list'
    detail_url = 'api-bom-report-detail'
    print_url = 'api-bom-report-print'

    def setUp(self):
        """Setup function for the bill of materials Report"""
        self.copyReportTemplate('inventree_bill_of_materials_report.html', 'bill of materials report')

        return super().setUp()


class PurchaseOrderReportTest(ReportTest):
    """Unit test class for the PurchaseOrderReport model"""
    model = report_models.PurchaseOrderReport

    list_url = 'api-po-report-list'
    detail_url = 'api-po-report-detail'
    print_url = 'api-po-report-print'

    def setUp(self):
        """Setup function for the purchase order Report"""
        self.copyReportTemplate('inventree_po_report.html', 'purchase order report')

        return super().setUp()


class SalesOrderReportTest(ReportTest):
    """Unit test class for the SalesOrderReport model"""
    model = report_models.SalesOrderReport

    list_url = 'api-so-report-list'
    detail_url = 'api-so-report-detail'
    print_url = 'api-so-report-print'

    def setUp(self):
        """Setup function for the sales order Report"""
        self.copyReportTemplate('inventree_so_report.html', 'sales order report')

        return super().setUp()


class ReturnOrderReportTest(ReportTest):
    """Unit tests for the ReturnOrderReport model"""

    model = report_models.ReturnOrderReport
    list_url = 'api-return-order-report-list'
    detail_url = 'api-return-order-report-detail'
    print_url = 'api-return-order-report-print'

    def setUp(self):
        """Setup function for the ReturnOrderReport tests"""
        self.copyReportTemplate('inventree_return_order_report.html', 'return order report')

        return super().setUp()


class StockLocationReportTest(ReportTest):
    """Unit tests for the StockLocationReport model"""

    model = report_models.StockLocationReport
    list_url = 'api-stocklocation-report-list'
    detail_url = 'api-stocklocation-report-detail'
    print_url = 'api-stocklocation-report-print'

    def setUp(self):
        """Setup function for the StockLocationReport tests"""
        self.copyReportTemplate('inventree_slr_report.html', 'stock location report')

        return super().setUp()
