#!/usr/bin/env python

"""
Given the output of doctest and a file, adjust the doctests so they won't fail.

Doctest failures due to exceptions are ignored.

AUTHORS::

- Nicolas M. Thiery <nthiery at users dot sf dot net>  Initial version (2008?)

- Andrew Mathas <andrew dot mathas at sydney dot edu dot au> 2013-02-14
  Cleaned up the code and hacked it so that the script can now cope with the
  situations when either the expected output or computed output are empty.
  Added doctest to sage.tests.cmdline

"""

import os, sys, difflib
from optparse import OptionParser
from sage.misc.temporary_file import tmp_dir

usage=r'''usage: sage --fixdoctests <source file> [doctest output] [--long]
Creates a file <source file>.out that passes the doctests (modulo any raised exceptions)'''
parser = OptionParser(usage=usage)
parser.add_option('-l','--long',dest='long', action="store_true", default=False)

options,args = parser.parse_args()

if not (len(args) in [1,2]) :
    print usage
    sys.exit(1)

# set input and output files
test_file=args[0]
test_output=args[1] if len(args)==2 else test_file+'.out'
try:
    with open(test_file,'r') as src: 
        src_in = src.read()
        pass
except IOError:
    raise ValueError('The file "%s" either does not exist or it is not readable' % test_file )
    sys.exit(1)

# put the output of the test into sage's temporary directory
doc_out=tmp_dir()+'.tmp'
os.system('sage -t %s %s > %s'%('--long' if options.long else '', test_file, doc_out))

doc_out = open(doc_out).read()
sep = "**********************************************************************\n"

doctests = doc_out.split(sep)
src_in_lines = src_in.splitlines()

for block in doctests:
    if not 'Failed example:' in block: continue  # sanity checking, but shouldn't happen

    # Extract the line, what was expected, and was got.
    line = block.find('line ')             # block should contain:  'line ##, in ...', where ## is an integer
    comma = block.find(', in ')            # we try to extract the line number which gives the test failure
    if line == -1 or comma==-1: continue   # but if something goes wrong we give up
    line_num=eval(block[line+5:comma])
    if 'Expected nothing' in block:
        exp = block.find('Expected nothing')
        block='\n'+block[exp+17:]  # so that split('\nGot:\n') does not fail below
    elif 'Expected:' in block:
        exp = block.find('Expected:\n')
        block=block[exp+10:]
    else:
        continue
    if block[-21:]=='Got:\n    <BLANKLINE>\n':
        expected=block[:-22]
        got=['']
    else:
        expected, got = block.split('\nGot:\n')
        got = got.splitlines()      # got can't be the empty string


    # If we expected nothing, and got something, then we need to insert the line before line_num
    # and match indentation with line number line_num-1
    if expected=='':
        indent = (len(src_in_lines[line_num-1]) - len(src_in_lines[line_num-1].lstrip()))
        src_in_lines[line_num-1]+='\n'+'\n'.join('%s%s'%(' '*indent,line.lstrip()) for line in got)
        continue

    # Guess how much extra indenting got needs to match with the indentation
    # of src_in_lines - we match the indentation with the line in ``got`` which
    # has the smallest indentation after lstrip(). Note that the amount of indentation
    # required could be negative if the ``got`` block is indented. In this case
    # ``indent`` is set to zero.
    indent = max(0,(len(src_in_lines[line_num]) - len(src_in_lines[line_num].lstrip())
              - min(len(got[j]) - len(got[j].lstrip()) for j in range(len(got)))))

    expected=expected.splitlines()

    # Double check that what was expected was indeed in the source file and if
    # it is not then then print a warning for the user which contains the
    # problematic lines.
    if any( expected[i].strip() != src_in_lines[line_num+i].strip() for i in range(len(expected)) ):
        import warnings
        warnings.warn("Did not manage to replace\n%s\n%s\n%s\nwith\n%s\n%s\n%s"%(
                         '>'*40,'\n'.join(expected),'>'*40,'<'*40, '\n'.join(got), '<'*40))
        continue

    # If we got something when we expected nothing then we delete the line from the
    # output, otherwise, add all of what we `got` onto the end of src_in_lines[line_num]
    if got==['']:
        src_in_lines[line_num] = None
    else:
        src_in_lines[line_num] = '\n'.join( (' '*indent + got[i]) for i in range(len(got)) )

    # Mark any remaining `expected` lines as ``None`` so as to preserve the line numbering
    for i in range(1, len(expected)):
        src_in_lines[line_num+i] = None


# We're done, so (try and) create and write the output file
while True:
    try:
        open(test_output,'w').write('\n'.join( line for line in src_in_lines if line is not None ))
        break
    except IOError:
        test_output = raw_input('Cannot write to %s. Please give a new filename: ' % test_output)

# ...and print a diff of the old and new files to stdout
with open(test_file,'r') as src:
    with open(test_output,'r') as output:
        sys.stdout.writelines(line for line in difflib.unified_diff(src.readlines(),output.readlines()))


