"""Tests for the Formatters."""

import warnings
from math import pi

try:
    import numpy
except:
    numpy = None
import nose.tools as nt

from IPython import get_ipython
from traitlets.config import Config
from IPython.core.formatters import (
    PlainTextFormatter, HTMLFormatter, PDFFormatter, _mod_name_key,
    DisplayFormatter, JSONFormatter,
)
from IPython.utils.io import capture_output

class A(object):
    def __repr__(self):
        return 'A()'

class B(A):
    def __repr__(self):
        return 'B()'

class C:
    pass

class BadRepr(object):
    def __repr__(self):
        raise ValueError("bad repr")

class BadPretty(object):
    _repr_pretty_ = None

class GoodPretty(object):
    def _repr_pretty_(self, pp, cycle):
        pp.text('foo')

    def __repr__(self):
        return 'GoodPretty()'

def foo_printer(obj, pp, cycle):
    pp.text('foo')

def test_pretty():
    f = PlainTextFormatter()
    f.for_type(A, foo_printer)
    nt.assert_equal(f(A()), 'foo')
    nt.assert_equal(f(B()), 'foo')
    nt.assert_equal(f(GoodPretty()), 'foo')
    # Just don't raise an exception for the following:
    f(BadPretty())

    f.pprint = False
    nt.assert_equal(f(A()), 'A()')
    nt.assert_equal(f(B()), 'B()')
    nt.assert_equal(f(GoodPretty()), 'GoodPretty()')


def test_deferred():
    f = PlainTextFormatter()

def test_precision():
    """test various values for float_precision."""
    f = PlainTextFormatter()
    nt.assert_equal(f(pi), repr(pi))
    f.float_precision = 0
    if numpy:
        po = numpy.get_printoptions()
        nt.assert_equal(po['precision'], 0)
    nt.assert_equal(f(pi), '3')
    f.float_precision = 2
    if numpy:
        po = numpy.get_printoptions()
        nt.assert_equal(po['precision'], 2)
    nt.assert_equal(f(pi), '3.14')
    f.float_precision = '%g'
    if numpy:
        po = numpy.get_printoptions()
        nt.assert_equal(po['precision'], 2)
    nt.assert_equal(f(pi), '3.14159')
    f.float_precision = '%e'
    nt.assert_equal(f(pi), '3.141593e+00')
    f.float_precision = ''
    if numpy:
        po = numpy.get_printoptions()
        nt.assert_equal(po['precision'], 8)
    nt.assert_equal(f(pi), repr(pi))

def test_bad_precision():
    """test various invalid values for float_precision."""
    f = PlainTextFormatter()
    def set_fp(p):
        f.float_precision=p
    nt.assert_raises(ValueError, set_fp, '%')
    nt.assert_raises(ValueError, set_fp, '%.3f%i')
    nt.assert_raises(ValueError, set_fp, 'foo')
    nt.assert_raises(ValueError, set_fp, -1)

def test_for_type():
    f = PlainTextFormatter()
    
    # initial return, None
    nt.assert_is(f.for_type(C, foo_printer), None)
    # no func queries
    nt.assert_is(f.for_type(C), foo_printer)
    # shouldn't change anything
    nt.assert_is(f.for_type(C), foo_printer)
    # None should do the same
    nt.assert_is(f.for_type(C, None), foo_printer)
    nt.assert_is(f.for_type(C, None), foo_printer)

def test_for_type_string():
    f = PlainTextFormatter()
    
    type_str = '%s.%s' % (C.__module__, 'C')
    
    # initial return, None
    nt.assert_is(f.for_type(type_str, foo_printer), None)
    # no func queries
    nt.assert_is(f.for_type(type_str), foo_printer)
    nt.assert_in(_mod_name_key(C), f.deferred_printers)
    nt.assert_is(f.for_type(C), foo_printer)
    nt.assert_not_in(_mod_name_key(C), f.deferred_printers)
    nt.assert_in(C, f.type_printers)

def test_for_type_by_name():
    f = PlainTextFormatter()
    
    mod = C.__module__
    
    # initial return, None
    nt.assert_is(f.for_type_by_name(mod, 'C', foo_printer), None)
    # no func queries
    nt.assert_is(f.for_type_by_name(mod, 'C'), foo_printer)
    # shouldn't change anything
    nt.assert_is(f.for_type_by_name(mod, 'C'), foo_printer)
    # None should do the same
    nt.assert_is(f.for_type_by_name(mod, 'C', None), foo_printer)
    nt.assert_is(f.for_type_by_name(mod, 'C', None), foo_printer)

def test_lookup():
    f = PlainTextFormatter()
    
    f.for_type(C, foo_printer)
    nt.assert_is(f.lookup(C()), foo_printer)
    with nt.assert_raises(KeyError):
        f.lookup(A())

def test_lookup_string():
    f = PlainTextFormatter()
    type_str = '%s.%s' % (C.__module__, 'C')
    
    f.for_type(type_str, foo_printer)
    nt.assert_is(f.lookup(C()), foo_printer)
    # should move from deferred to imported dict
    nt.assert_not_in(_mod_name_key(C), f.deferred_printers)
    nt.assert_in(C, f.type_printers)

def test_lookup_by_type():
    f = PlainTextFormatter()
    f.for_type(C, foo_printer)
    nt.assert_is(f.lookup_by_type(C), foo_printer)
    with nt.assert_raises(KeyError):
        f.lookup_by_type(A)

def test_lookup_by_type_string():
    f = PlainTextFormatter()
    type_str = '%s.%s' % (C.__module__, 'C')
    f.for_type(type_str, foo_printer)
    
    # verify insertion
    nt.assert_in(_mod_name_key(C), f.deferred_printers)
    nt.assert_not_in(C, f.type_printers)
    
    nt.assert_is(f.lookup_by_type(type_str), foo_printer)
    # lookup by string doesn't cause import
    nt.assert_in(_mod_name_key(C), f.deferred_printers)
    nt.assert_not_in(C, f.type_printers)
    
    nt.assert_is(f.lookup_by_type(C), foo_printer)
    # should move from deferred to imported dict
    nt.assert_not_in(_mod_name_key(C), f.deferred_printers)
    nt.assert_in(C, f.type_printers)

def test_in_formatter():
    f = PlainTextFormatter()
    f.for_type(C, foo_printer)
    type_str = '%s.%s' % (C.__module__, 'C')
    nt.assert_in(C, f)
    nt.assert_in(type_str, f)

def test_string_in_formatter():
    f = PlainTextFormatter()
    type_str = '%s.%s' % (C.__module__, 'C')
    f.for_type(type_str, foo_printer)
    nt.assert_in(type_str, f)
    nt.assert_in(C, f)

def test_pop():
    f = PlainTextFormatter()
    f.for_type(C, foo_printer)
    nt.assert_is(f.lookup_by_type(C), foo_printer)
    nt.assert_is(f.pop(C, None), foo_printer)
    f.for_type(C, foo_printer)
    nt.assert_is(f.pop(C), foo_printer)
    with nt.assert_raises(KeyError):
        f.lookup_by_type(C)
    with nt.assert_raises(KeyError):
        f.pop(C)
    with nt.assert_raises(KeyError):
        f.pop(A)
    nt.assert_is(f.pop(A, None), None)

def test_pop_string():
    f = PlainTextFormatter()
    type_str = '%s.%s' % (C.__module__, 'C')
    
    with nt.assert_raises(KeyError):
        f.pop(type_str)
    
    f.for_type(type_str, foo_printer)
    f.pop(type_str)
    with nt.assert_raises(KeyError):
        f.lookup_by_type(C)
    with nt.assert_raises(KeyError):
        f.pop(type_str)

    f.for_type(C, foo_printer)
    nt.assert_is(f.pop(type_str, None), foo_printer)
    with nt.assert_raises(KeyError):
        f.lookup_by_type(C)
    with nt.assert_raises(KeyError):
        f.pop(type_str)
    nt.assert_is(f.pop(type_str, None), None)
    

def test_error_method():
    f = HTMLFormatter()
    class BadHTML(object):
        def _repr_html_(self):
            raise ValueError("Bad HTML")
    bad = BadHTML()
    with capture_output() as captured:
        result = f(bad)
    nt.assert_is(result, None)
    nt.assert_in("Traceback", captured.stdout)
    nt.assert_in("Bad HTML", captured.stdout)
    nt.assert_in("_repr_html_", captured.stdout)

def test_nowarn_notimplemented():
    f = HTMLFormatter()
    class HTMLNotImplemented(object):
        def _repr_html_(self):
            raise NotImplementedError
    h = HTMLNotImplemented()
    with capture_output() as captured:
        result = f(h)
    nt.assert_is(result, None)
    nt.assert_equal("", captured.stderr)
    nt.assert_equal("", captured.stdout)

def test_warn_error_for_type():
    f = HTMLFormatter()
    f.for_type(int, lambda i: name_error)
    with capture_output() as captured:
        result = f(5)
    nt.assert_is(result, None)
    nt.assert_in("Traceback", captured.stdout)
    nt.assert_in("NameError", captured.stdout)
    nt.assert_in("name_error", captured.stdout)

def test_error_pretty_method():
    f = PlainTextFormatter()
    class BadPretty(object):
        def _repr_pretty_(self):
            return "hello"
    bad = BadPretty()
    with capture_output() as captured:
        result = f(bad)
    nt.assert_is(result, None)
    nt.assert_in("Traceback", captured.stdout)
    nt.assert_in("_repr_pretty_", captured.stdout)
    nt.assert_in("given", captured.stdout)
    nt.assert_in("argument", captured.stdout)


def test_bad_repr_traceback():
    f = PlainTextFormatter()
    bad = BadRepr()
    with capture_output() as captured:
        result = f(bad)
    # catches error, returns None
    nt.assert_is(result, None)
    nt.assert_in("Traceback", captured.stdout)
    nt.assert_in("__repr__", captured.stdout)
    nt.assert_in("ValueError", captured.stdout)


class MakePDF(object):
    def _repr_pdf_(self):
        return 'PDF'

def test_pdf_formatter():
    pdf = MakePDF()
    f = PDFFormatter()
    nt.assert_equal(f(pdf), 'PDF')

def test_print_method_bound():
    f = HTMLFormatter()
    class MyHTML(object):
        def _repr_html_(self):
            return "hello"
    with capture_output() as captured:
        result = f(MyHTML)
    nt.assert_is(result, None)
    nt.assert_not_in("FormatterWarning", captured.stderr)

    with capture_output() as captured:
        result = f(MyHTML())
    nt.assert_equal(result, "hello")
    nt.assert_equal(captured.stderr, "")

def test_print_method_weird():

    class TextMagicHat(object):
        def __getattr__(self, key):
            return key

    f = HTMLFormatter()
    
    text_hat = TextMagicHat()
    nt.assert_equal(text_hat._repr_html_, '_repr_html_')
    with capture_output() as captured:
        result = f(text_hat)
    
    nt.assert_is(result, None)
    nt.assert_not_in("FormatterWarning", captured.stderr)

    class CallableMagicHat(object):
        def __getattr__(self, key):
            return lambda : key
    
    call_hat = CallableMagicHat()
    with capture_output() as captured:
        result = f(call_hat)
    
    nt.assert_equal(result, None)

    class BadReprArgs(object):
        def _repr_html_(self, extra, args):
            return "html"
    
    bad = BadReprArgs()
    with capture_output() as captured:
        result = f(bad)
    
    nt.assert_is(result, None)
    nt.assert_not_in("FormatterWarning", captured.stderr)


def test_format_config():
    """config objects don't pretend to support fancy reprs with lazy attrs"""
    f = HTMLFormatter()
    cfg = Config()
    with capture_output() as captured:
        result = f(cfg)
    nt.assert_is(result, None)
    nt.assert_equal(captured.stderr, "")

    with capture_output() as captured:
        result = f(Config)
    nt.assert_is(result, None)
    nt.assert_equal(captured.stderr, "")

def test_pretty_max_seq_length():
    f = PlainTextFormatter(max_seq_length=1)
    lis = list(range(3))
    text = f(lis)
    nt.assert_equal(text, '[0, ...]')
    f.max_seq_length = 0
    text = f(lis)
    nt.assert_equal(text, '[0, 1, 2]')
    text = f(list(range(1024)))
    lines = text.splitlines()
    nt.assert_equal(len(lines), 1024)


def test_ipython_display_formatter():
    """Objects with _ipython_display_ defined bypass other formatters"""
    f = get_ipython().display_formatter
    catcher = []
    class SelfDisplaying(object):
        def _ipython_display_(self):
            catcher.append(self)

    class NotSelfDisplaying(object):
        def __repr__(self):
            return "NotSelfDisplaying"
        
        def _ipython_display_(self):
            raise NotImplementedError
    
    save_enabled = f.ipython_display_formatter.enabled
    f.ipython_display_formatter.enabled = True
    
    yes = SelfDisplaying()
    no = NotSelfDisplaying()
    
    d, md = f.format(no)
    nt.assert_equal(d, {'text/plain': repr(no)})
    nt.assert_equal(md, {})
    nt.assert_equal(catcher, [])
    
    d, md = f.format(yes)
    nt.assert_equal(d, {})
    nt.assert_equal(md, {})
    nt.assert_equal(catcher, [yes])

    f.ipython_display_formatter.enabled = save_enabled


def test_json_as_string_deprecated():
    class JSONString(object):
        def _repr_json_(self):
            return '{}'
    
    f = JSONFormatter()
    with warnings.catch_warnings(record=True) as w:
        d = f(JSONString())
    nt.assert_equal(d, {})
    nt.assert_equal(len(w), 1)


def test_repr_mime():
    class HasReprMime(object):
        def _repr_mimebundle_(self, include=None, exclude=None):
            return {
                'application/json+test.v2': {
                    'x': 'y'
                },
                'plain/text' : '<HasReprMime>',
                'image/png' : 'i-overwrite'
            }

        def _repr_png_(self):
            return 'should-be-overwritten'
        def _repr_html_(self):
            return '<b>hi!</b>'
    
    f = get_ipython().display_formatter
    html_f = f.formatters['text/html']
    save_enabled = html_f.enabled
    html_f.enabled = True
    obj = HasReprMime()
    d, md = f.format(obj)
    html_f.enabled = save_enabled
    
    nt.assert_equal(sorted(d), ['application/json+test.v2',
                                'image/png',
                                'plain/text',
                                'text/html',
                                'text/plain'])
    nt.assert_equal(md, {})

    d, md = f.format(obj, include={'image/png'})
    nt.assert_equal(list(d.keys()), ['image/png'],
                    'Include should filter out even things from repr_mimebundle')
    nt.assert_equal(d['image/png'], 'i-overwrite', '_repr_mimebundle_ take precedence')



def test_pass_correct_include_exclude():
    class Tester(object):

        def __init__(self, include=None, exclude=None):
            self.include = include
            self.exclude = exclude

        def _repr_mimebundle_(self, include, exclude, **kwargs):
            if include and (include != self.include):
                raise ValueError('include got modified: display() may be broken.')
            if exclude and (exclude != self.exclude):
                raise ValueError('exclude got modified: display() may be broken.')

            return None

    include = {'a', 'b', 'c'}
    exclude = {'c', 'e' , 'f'}

    f = get_ipython().display_formatter
    f.format(Tester(include=include, exclude=exclude), include=include, exclude=exclude)
    f.format(Tester(exclude=exclude), exclude=exclude)
    f.format(Tester(include=include), include=include)


def test_repr_mime_meta():
    class HasReprMimeMeta(object):
        def _repr_mimebundle_(self, include=None, exclude=None):
            data = {
                'image/png': 'base64-image-data',
            }
            metadata = {
                'image/png': {
                    'width': 5,
                    'height': 10,
                }
            }
            return (data, metadata)
    
    f = get_ipython().display_formatter
    obj = HasReprMimeMeta()
    d, md = f.format(obj)
    nt.assert_equal(sorted(d), ['image/png', 'text/plain'])
    nt.assert_equal(md, {
        'image/png': {
            'width': 5,
            'height': 10,
        }
    })
