#!/usr/bin/env python3

import base64
import datetime
import sys
import time
import unittest
import uuid
import lxml.etree as ET
from io import BytesIO
from pathlib import Path

sys.path.append('..')
from pycert import SATCertificate
from finkok import PACFinkok


NAME = 'finkok'


TEMPLATE_CFDI = """<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" LugarExpedicion="06850" Moneda="MXN" SubTotal="10000.00" TipoCambio="1" TipoDeComprobante="I" Total="11600.00" FormaPago="01" MetodoPago="PUE" Version="3.3" xsi:schemaLocation="http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv33.xsd">
  <cfdi:Emisor Rfc="EKU9003173C9" RegimenFiscal="601"/>
  <cfdi:Receptor Rfc="BASM740115RW0" UsoCFDI="G01"/>
  <cfdi:Conceptos>
    <cfdi:Concepto Cantidad="1.0" ClaveProdServ="60121001" ClaveUnidad="ACT" Descripcion="Asesoría en desarrollo" Importe="10000.00" ValorUnitario="10000.00">
      <cfdi:Impuestos>
        <cfdi:Traslados>
          <cfdi:Traslado Base="10000.00" Importe="1600.00" Impuesto="002" TasaOCuota="0.160000" TipoFactor="Tasa"/>
        </cfdi:Traslados>
      </cfdi:Impuestos>
    </cfdi:Concepto>
  </cfdi:Conceptos>
  <cfdi:Impuestos TotalImpuestosTrasladados="1600.00">
    <cfdi:Traslados>
      <cfdi:Traslado Importe="1600.00" Impuesto="002" TasaOCuota="0.160000" TipoFactor="Tasa"/>
    </cfdi:Traslados>
  </cfdi:Impuestos>
</cfdi:Comprobante>
"""


TEMPLATE_CANCEL = """<Cancelacion RfcEmisor="{rfc}" Fecha="{fecha}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://cancelacfd.sat.gob.mx">
  <Folios>
    <UUID>{uuid}</UUID>
  </Folios>
  <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
      <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
      <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
      <Reference URI="">
        <Transforms>
          <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
        </Transforms>
        <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
        <DigestValue />
      </Reference>
    </SignedInfo>
    <SignatureValue />
    <KeyInfo>
      <X509Data>
        <X509SubjectName />
        <X509IssuerSerial />
        <X509Certificate />
      </X509Data>
      <KeyValue />
    </KeyInfo>
  </Signature>
</Cancelacion>
"""


class TestCfdi(object):

    def __init__(self):
        self._xml = ''
        self._make_cfdi()

    @property
    def xml(self):
        return self._xml.decode()

    def _make_cfdi(self):
        path = Path(__file__)
        path_cer = Path(path.parent).joinpath('certificados', f'{NAME}.cer')
        path_key = Path(path.parent).joinpath('certificados', f'{NAME}.enc')
        path_xslt = Path(path.parent).joinpath('xslt', 'cadena.xslt')
        self._cer_ori = cer = path_cer.read_bytes()
        self._key_ori = key = path_key.read_bytes()

        self._cert = SATCertificate(cer, key)
        self._doc = ET.parse(BytesIO(TEMPLATE_CFDI.encode()))
        self._root = self._doc.getroot()
        self._root.attrib['Fecha'] = datetime.datetime.now().isoformat()[:19]
        self._root.attrib['NoCertificado'] = self._cert.serial_number
        self._root.attrib['Certificado'] = self._cert.cer_txt

        self._add_stamp(path_xslt)

        self._xml = ET.tostring(self._root,
            pretty_print=True, doctype='<?xml version="1.0" encoding="utf-8"?>')
        return

    def _add_stamp(self, path_xslt):
        xslt = open(path_xslt, 'rb')
        transfor = ET.XSLT(ET.parse(xslt))
        cadena = str(transfor(self._doc)).encode()
        stamp = self._cert.sign(cadena)
        self._root.attrib['Sello'] = stamp
        xslt.close()
        return

    def sign_xml(self, template):
        tree = ET.fromstring(template.encode())
        tree = self._cert.sign_xml(tree)
        xml = ET.tostring(tree).decode()
        return xml

    @property
    def cert(self):
        cer = base64.b64encode(self._cer_ori).decode()
        key = base64.b64encode(self._key_ori).decode()
        return key, cer


class TestStamp(unittest.TestCase):

    def setUp(self):
        print(f'In method: {self._testMethodName}')
        self.pac = PACFinkok()

    def test_cfdi_stamp(self):
        cfdi = TestCfdi().xml
        result = self.pac.stamp(cfdi)
        cfdi_uuid = result['uuid']

        self.assertFalse(bool(self.pac.error))
        self.assertTrue(bool(uuid.UUID(cfdi_uuid)))

    def test_cfdi_cancel(self):
        expected = '201'
        cfdi = TestCfdi()
        result = self.pac.stamp(cfdi.xml)
        cfdi_uuid = self.pac.cfdi_uuid

        self.assertFalse(bool(self.pac.error))
        self.assertTrue(bool(uuid.UUID(cfdi_uuid)))

        time.sleep(1)
        cert = cfdi.cert
        info = {
            'key': cert[0],
            'cer': cert[1],
            'pass': '12345678a',
            'tipo': 'cfdi3.3',
        }
        result = self.pac.cancel(result, info)
        self.assertFalse(bool(self.pac.error))

        tree = ET.fromstring(result)
        cancel_uuid = tree.xpath('string(//Acuse/Folios/UUID)')
        status = tree.xpath('string(//Acuse/Folios/EstatusUUID)')

        self.assertEqual(cfdi_uuid, cancel_uuid)
        self.assertEqual(status, expected)

    def test_cfdi_cancel_xml(self):
        expected = '201'
        cfdi = TestCfdi()
        result = self.pac.stamp(cfdi.xml)
        cfdi_uuid = self.pac.cfdi_uuid

        self.assertFalse(bool(self.pac.error))
        self.assertTrue(bool(uuid.UUID(cfdi_uuid)))

        NS_CFDI = {
            'cfdi': 'http://www.sat.gob.mx/cfd/3',
            'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
        }
        tree = ET.fromstring(result.encode())
        rfc_emisor = tree.xpath(
            'string(//cfdi:Comprobante/cfdi:Emisor/@Rfc)',
            namespaces=NS_CFDI)

        time.sleep(1)
        data = {
            'rfc': rfc_emisor,
            'fecha': datetime.datetime.now().isoformat()[:19],
            'uuid': cfdi_uuid,
        }
        template = TEMPLATE_CANCEL.format(**data)
        sign_xml = cfdi.sign_xml(template)
        info = {
            'tipo': 'cfdi3.3',
        }
        result = self.pac.cancel_xml(result, sign_xml, info)
        tree = ET.fromstring(result)
        uid = tree.xpath('string(//Acuse/Folios/UUID)')
        status = tree.xpath('string(//Acuse/Folios/EstatusUUID)')

        self.assertEqual(cfdi_uuid, uid)
        self.assertEqual(status, expected)

    def test_cfdi_status(self):
        expected = ''
        cfdi = TestCfdi()
        result = self.pac.stamp(cfdi.xml)
        cfdi_uuid = self.pac.cfdi_uuid

        self.assertFalse(bool(self.pac.error))
        self.assertTrue(bool(uuid.UUID(cfdi_uuid)))

        NS_CFDI = {
            'cfdi': 'http://www.sat.gob.mx/cfd/3',
            'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
        }
        tree = ET.fromstring(result.encode())
        rfc_emisor = tree.xpath(
            'string(//cfdi:Comprobante/cfdi:Emisor/@Rfc)',
            namespaces=NS_CFDI)
        rfc_receptor = tree.xpath(
            'string(//cfdi:Comprobante/cfdi:Receptor/@Rfc)',
            namespaces=NS_CFDI)
        total = tree.xpath(
            'string(//cfdi:Comprobante/@Total)',
            namespaces=NS_CFDI)

        time.sleep(3)
        data = {
            'rfc_receptor': rfc_receptor,
            'rfc_emisor': rfc_emisor,
            'total': total,
            'uuid': cfdi_uuid,
        }
        result = self.pac.status(data)
        self.assertEqual(result, expected)


if __name__ == '__main__':
    unittest.main()
