#!/usr/bin/env python
# -*- coding: utf-8 -*-

from io import open

import os
import re
import sys
import shlex
import locale
import hashlib
import argparse
import subprocess

from difflib import unified_diff

class DocTests:
  def __init__(self, args):
    scriptpath      = os.path.dirname(os.path.realpath(__file__))
    self.ledger     = os.path.realpath(args.ledger)
    self.sourcepath = os.path.realpath(args.file)
    self.verbose    = args.verbose
    self.tests      = args.examples

    self.examples       = dict()
    self.test_files     = list()
    self.testin_token   = 'command'
    self.testout_token  = 'output'
    self.testdat_token  = 'input'
    self.testfile_token = 'file'
    self.validate_token = 'validate'
    self.validate_cmd_token = 'validate-command'
    self.validate_dat_token = 'validate-data'
    self.testwithdat_token  = 'with_input'
    self.testwithfile_token = 'with_file'

  def read_example(self):
    endexample = re.compile(r'^@end\s+smallexample\s*$')
    example = str()
    while True:
      line = self.file.readline()
      self.current_line += 1
      if len(line) <= 0 or endexample.match(line): break
      # Replace special texinfo character sequences with their ASCII counterpart
      example += re.sub(r'@([@{}])', r'\1', line)
    return example

  def test_id(self, example):
    example_id = example.rstrip().encode('utf-8')
    return hashlib.sha1(example_id).hexdigest()[0:7].upper()

  def find_examples(self):
    startexample = re.compile(r'^@smallexample\s+@c\s+(%s|%s|%s|%s)(?::([\dA-Fa-f]+|validate))?(?:,(.*))?'
        % (self.testin_token, self.testout_token, self.testdat_token, self.testfile_token))
    while True:
      line = self.file.readline()
      self.current_line += 1
      if len(line) <= 0: break

      startmatch = startexample.match(line)
      if (startmatch):
        test_begin_pos = self.file.tell()
        test_begin_line = self.current_line
        test_kind = startmatch.group(1)
        test_id = startmatch.group(2)
        test_options = dict()
        for pair in re.split(r',\s*', str(startmatch.group(3))):
          kv = re.split(r':\s*', pair, 2)
          try:
            test_options[kv[0]] = kv[1]
          except IndexError:
            pass
        example = self.read_example()
        test_end_pos = self.file.tell()
        test_end_line = self.current_line

        if not test_id:
          print('Example', test_kind, 'in line', test_begin_line, 'is missing id.', file=sys.stderr)
          test_id = self.test_id(example)
          if test_kind == self.testin_token:
            print('Use', self.test_id(example), file=sys.stderr)
        elif test_kind == self.testin_token and test_id != self.validate_token and test_id != self.test_id(example):
          print('Expected test id', test_id, 'for example' \
              , test_kind, 'on line', test_begin_line, 'to be', self.test_id(example), file=sys.stderr)

        if test_id == self.validate_token:
          test_id = "Val-" + str(test_begin_line)
          if test_kind == self.testin_token:
            test_kind = self.validate_cmd_token
          elif test_kind == self.testdat_token:
            test_kind = self.validate_dat_token
        try:
          self.examples[test_id]
        except KeyError:
          self.examples[test_id] = dict()

        try:
          example = self.examples[test_id][test_kind][test_kind] + example
        except KeyError:
          pass

        self.examples[test_id][test_kind] = {
            'bpos': test_begin_pos,
            'epos': test_end_pos,
            'blin': test_begin_line,
            'elin': test_end_line,
            'opts': test_options,
            test_kind: example,
            }

  def parse_command(self, test_id, example):
    validate_command = False
    try:
      command = example[self.testin_token][self.testin_token]
      command = re.sub(r'\\\n', '', command)
    except KeyError:
      if self.validate_dat_token in example:
        command = '$ ledger bal'
      elif self.validate_cmd_token in example:
        validate_command = True
        command = example[self.validate_cmd_token][self.validate_cmd_token]
      else:
        return None

    if sys.version_info.major == 2:
      command = command.encode(locale.getpreferredencoding())
    command_parts = shlex.split(command)
    command = filter(lambda x: x != '\n', command_parts)
    if sys.version_info.major > 2:
        command = list(command)
    if command[0] == '$': command.remove('$')
    index = command.index('ledger')
    command[index] = self.ledger
    for i,argument in enumerate(shlex.split('--args-only --columns 80')):
      command.insert(index+i+1, argument)

    try:
      findex = command.index('-f')
    except ValueError:
      try:
        findex = command.index('--file')
      except ValueError:
        findex = index+1
        command.insert(findex, '--file')
        if validate_command:
          command.insert(findex+1, 'sample.dat')
        else:
          command.insert(findex+1, test_id + '.dat')
    return (command, findex+1)

  def test_examples(self):
    failed = set()
    tests = self.examples.keys()
    if self.tests:
      tests = list(set(self.tests).intersection(tests))
      temp = list(set(self.tests).difference(tests))
      if len(temp) > 0:
        print('Skipping non-existent examples: %s' % ', '.join(temp), file=sys.stderr)

    for test_id in tests:
      validation = False
      if self.validate_dat_token in self.examples[test_id] or self.validate_cmd_token in self.examples[test_id]:
        validation = True
      example = self.examples[test_id]
      try:
        (command, findex) = self.parse_command(test_id, example)
      except TypeError:
        failed.add(test_id)
        continue

      output = example.get(self.testout_token, {}).get(self.testout_token)
      input = example.get(self.testdat_token, {}).get(self.testdat_token)
      if not input:
        with_input = example.get(self.testin_token, {}).get('opts', {}).get(self.testwithdat_token)
        input = self.examples.get(with_input, {}).get(self.testdat_token, {}).get(self.testdat_token)
        if not input:
          input = example.get(self.validate_dat_token, {}).get(self.validate_dat_token)

      if command and (output != None or validation):
        test_file_created = False
        if findex:
          scriptpath = os.path.dirname(os.path.realpath(__file__))
          test_input_dir = os.path.join(scriptpath, '..', 'test', 'input')
          test_file = command[findex]
          if not os.path.exists(test_file):
            if input:
              test_file_created = True
              with open(test_file, 'w', encoding='utf-8') as f:
                f.write(input)
            elif os.path.exists(os.path.join(test_input_dir, test_file)):
              command[findex] = os.path.join(test_input_dir, test_file)
        try:
          convert_idx  = command.index(str('convert'))
          convert_file = command[convert_idx+1]
          convert_data = example[self.testfile_token][self.testfile_token]
          if not os.path.exists(convert_file):
              with open(convert_file, 'w', encoding='utf-8') as f:
                f.write(convert_data)
        except ValueError:
         pass
        error = None
        try:
          verify = subprocess.check_output(command, stderr=subprocess.STDOUT)
          verify = verify.decode('utf-8')
          if sys.platform == 'win32':
            verify = verify.replace('\r\n', '\n')
          valid = (output == verify) or (not error and validation)
        except subprocess.CalledProcessError as e:
          error = e.output
          valid = False
          failed.add(test_id)
        if valid and test_file_created:
          os.remove(test_file)
        if self.verbose > 0:
          print(test_id, ':', 'Passed' if valid else 'FAILED: {}'.format(error) if error else 'FAILED')
        else:
          sys.stdout.write('.' if valid else 'E')
          sys.stdout.flush()

        if not (valid or error):
          failed.add(test_id)
          if self.verbose > 1:
            print(' '.join(command))
            if not validation:
              for line in unified_diff(output.split('\n'), verify.split('\n'), fromfile='generated', tofile='expected'):
                print(line)
              print()
      else:
        if self.verbose > 0:
          print(test_id, ':', 'Skipped')
        else:
          sys.stdout.write('X')

    if self.verbose == 0:
      print()
    if len(failed) > 0:
      print("\nThe following examples failed:")
      print(" ", "\n  ".join(failed))
    return len(failed)

  def main(self):
    self.file = open(self.sourcepath, encoding='utf-8')
    self.current_line = 0
    self.find_examples()
    failed_examples = self.test_examples()
    self.file.close()
    return failed_examples

if __name__ == "__main__":
  def getargs():
    parser = argparse.ArgumentParser(prog='DocTests',
            description='Test and validate ledger examples from the texinfo manual')
    parser.add_argument('-v', '--verbose',
        dest='verbose',
        action='count',
        default=0,
        help='be verbose. Add -vv for more verbosity')
    parser.add_argument('-l', '--ledger',
        dest='ledger',
        type=str,
        action='store',
        required=True,
        help='the path to the ledger executable to test with')
    parser.add_argument('-f', '--file',
        dest='file',
        type=str,
        action='store',
        required=True,
        help='the texinfo documentation file to run the examples from')
    parser.add_argument('examples',
        metavar='EXAMPLE',
        type=str,
        nargs='*',
        help='the examples to test')
    return parser.parse_args()

  args = getargs()
  script = DocTests(args)
  status = script.main()
  sys.exit(status)
