# Copyright (C) 2017 The Meme Factory, Inc.  http://www.meme.com/
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Karl O. Pinc <kop@meme.com>

import re

from enforcer import printout
from enforcer import report
from enforcer import fstree


#
# Helper classes
#
class MockDirEntry(object):
    def __init__(self, name):
        super(MockDirEntry, self).__init__()
        self.name = name
        self.path = None


class MockFileDirEntry(MockDirEntry):
    def is_dir(self):
        return False


class MockDirDirEntry(MockDirEntry):
    def is_dir(self):
        return True


class MockFSDir(object):
    def __init__(self, children={}):
        super(MockFSDir, self).__init__()
        self.children = children


#
# Function tests
#

# render_headers()

def test_render_headers_first():
    '''First header has no indentation'''
    out = report.render_headers(['first'])
    assert out == 'first'


def test_render_headers_second():
    '''Second header has 1 level of indentation'''
    out = report.render_headers(['first', 'second'])
    lines = out.split('\n')
    assert lines[-1] == report.LEVEL_INDENT + 'second'


def test_render_headers_third():
    '''Third header has 2 levels of indentation'''
    out = report.render_headers(['first', 'second', 'third'])
    lines = out.split('\n')
    assert lines[-1] == report.LEVEL_INDENT * 2 + 'third'


# visual_indentation()

def test_visual_indentation1():
    '''Does a single header get a single indentation?'''
    assert report.visual_indentation(['first']) == [report.LEVEL_INDENT]


def test_visual_indentation3():
    '''Do 3 levels of header get 3 levels of indentation?'''
    assert (report.visual_indentation(['first', 'second', 'third']) ==
            [report.LEVEL_INDENT] * 3)


# print_error()

def test_print_error_headers():
    '''Headers are not changed by print_error() call'''
    orig_headers = ['first']
    headers = orig_headers[:]
    report.print_error(printout.Printer(), headers, 'second', 'my msg')
    assert headers == orig_headers


def test_print_error_name(capsys):
    '''The name shows up in the print_error() output'''

    report.print_error(printout.Printer(), ['first'], 'second', 'my msg')
    (out, err) = capsys.readouterr()
    lines = out.split('\n')
    assert report.LEVEL_INDENT + 'second' in lines


def test_print_error_msg(capsys):
    '''The msg shows up in the print_error() output'''

    report.print_error(printout.Printer(), ['first'], 'second', 'my msg')
    (out, err) = capsys.readouterr()
    lines = out.split('\n')
    assert 'my msg' in lines


# match_errmsg()

def test_match_errmsg_msg():
    '''msg shows up on match_errmsg() output'''
    patloc = fstree.PatternLoc(2, 'pattern', 7)
    lines = report.match_errmsg('my msg', patloc).split('\n')
    assert 'my msg' in lines


# plural()

def test_plural_singular():
    '''We get no suffix when given a single item'''
    assert report.plural(['one']) == ''


def test_plural_plural():
    '''We get a suffix when given multiple items'''
    assert report.plural(['one', 'two']) == 's'


# sort_patlocs()

def test_sort_patlocs_single():
    '''A single patloc sorts to itself'''
    patlocs = set([fstree.PatternLoc(2, 'pattern1', 8)])
    sorted = report.sort_patlocs(patlocs)
    assert list(patlocs) == sorted


def test_sort_patlocs_by_row():
    '''Do patlocs sort by row?'''
    patlocs = set([fstree.PatternLoc(1, 'pattern1', 3),
                   fstree.PatternLoc(3, 'pattern3', 2),
                   fstree.PatternLoc(2, 'pattern2', 1)])
    sorted = report.sort_patlocs(patlocs)
    assert [pl.rownum for pl in sorted] == [1, 2, 3]


def test_sort_patlocs_by_row_by_pos():
    '''Do patlocs sort by row by pos?'''
    patlocs = set([fstree.PatternLoc(1, 'pattern1', 3),
                   fstree.PatternLoc(3, 'pattern3', 5),
                   fstree.PatternLoc(3, 'pattern3', 2),
                   fstree.PatternLoc(1, 'pattern1', 0),
                   fstree.PatternLoc(2, 'pattern2', 1)])
    sorted = report.sort_patlocs(patlocs)
    assert ([(pl.rownum, pl.pos) for pl in sorted] ==
            [(1, 0), (1, 3), (2, 1), (3, 2), (3, 5)])


# least_pos_per_row()

def test_least_pos_per_row_one():
    '''Sorting a single patloc uniquely by row returns itself'''
    patlocs = set([fstree.PatternLoc(2, 'pattern1', 8)])
    sorted = report.least_pos_per_row(patlocs)
    assert list(patlocs) == sorted


def test_least_pos_per_row_many():
    '''The result is unique by row, smallest pos is chosen'''
    patlocs = set([fstree.PatternLoc(1, 'pattern1', 3),
                   fstree.PatternLoc(3, 'pattern3', 5),
                   fstree.PatternLoc(3, 'pattern3', 2),
                   fstree.PatternLoc(1, 'pattern1', 0),
                   fstree.PatternLoc(2, 'pattern2', 1)])
    sorted = report.least_pos_per_row(patlocs)
    assert ([(pl.rownum, pl.pos) for pl in sorted] ==
            [(1, 0), (2, 1), (3, 2)])


# mismatched_rows_msg()

def test_mismatched_rows_msg():
    '''The patloc's row number shows up in the message'''
    assert re.search('128',
                     report.mismatched_rows_msg(
                         [fstree.PatternLoc(128, 'pattern', 3)]))


# matched_visual_pointer()

def test_matched_visual_pointer():
    '''Do we get the pointer we expect?'''
    assert report.matched_visual_pointer('', 3) == '   ^'


# render_failures()

def test_render_failures_nomerge():
    '''Rendering 3 patlocs with different positions produces 6 lines'''
    # When constructing the test remember that a patloc forces pos
    # values longer than the pattern length to the pattern length.
    # So you don't get different positions when exceeding the pattern length.
    output = report.render_failures(
        ['header'],
        [fstree.PatternLoc(3, 'pattern3', 2),
         fstree.PatternLoc(4, 'pattern4', 4),
         fstree.PatternLoc(5, 'pattern5', 6)])
    assert output.count('\n') == 5


def test_render_failures_merge():
    '''Rendering 3 patlocks with the same position produces 4 lines'''
    output = report.render_failures(
        ['header'],
        [fstree.PatternLoc(3, 'pattern3', 2),
         fstree.PatternLoc(4, 'pattern4', 2),
         fstree.PatternLoc(5, 'pattern5', 2)])
    assert output.count('\n') == 3


def test_render_failures_overlong():
    '''Rendering more than 5 patlocs replace the overage with ellipses'''
    patlocs = []
    for line in range(report.MAX_FAILS_SHOWN + 1):
        patlocs.append(
            fstree.PatternLoc(line, 'pattern{0}'.format(line), line))
    output = report.render_failures(['header'], patlocs)
    lines = output.split('\n')
    assert lines[-1] == report.LEVEL_INDENT + '...'


# mismatch_errmsg()

def test_mismatch_errmsg():
    '''Produces a string of at least 3 lines'''
    output = report.mismatch_errmsg(
        ['header'], '', [fstree.PatternLoc(3, 'pattern', 2)])
    assert output.count('\n') > 2


# report_dir_success()

def test_report_dir_success_error(capsys):
    '''Get an error, in the report on stdout, because the analyzer
    says the directory has no children'''

    printer = printout.Printer()
    analyzer = MockFSDir({})
    direntry = MockDirEntry('dirname')
    patloc = fstree.PatternLoc(3, 'dirname', 4)
    report.report_dir_success(
        printer, analyzer, set(), [], direntry, patloc, [])
    (out, err) = capsys.readouterr()
    assert out


def test_report_dir_success_recur(monkeypatch):
    '''(Try to) look at the directory's children'''
    # In python 3 we could use "nonlocal" instead of making a namespace
    class Namespace():
        pass

    ns = Namespace()
    ns.called = False

    def mock_dir_report(printer, analyzer, ignores, headers, direntry):
        ns.called = True
    monkeypatch.setattr(report, 'dir_report', mock_dir_report)

    printer = printout.Printer()
    analyzer = MockFSDir({3: None})
    patloc = fstree.PatternLoc(3, 'dirname', 4)
    report.report_dir_success(printer, analyzer, set(), [], None, patloc, [])
    assert ns.called


# report_file_success()

def test_report_file_success_error(capsys):
    '''Get an error, in the report on stdout, because the analyzer
    says the file has children'''

    printer = printout.Printer()
    analyzer = MockFSDir({3: None})
    direntry = MockDirEntry('filename')
    patloc = fstree.PatternLoc(3, 'filename', 4)
    report.report_file_success(printer, analyzer, [], direntry, patloc, [])
    (out, err) = capsys.readouterr()
    assert out
    assert err == ''


def test_report_file_success_noerror(capsys):
    '''Get no error, in the report on stdout, because the analyzer
    finds the file has no children'''
    printer = printout.Printer()
    analyzer = MockFSDir({})
    direntry = MockDirEntry('filename')
    patloc = fstree.PatternLoc(3, 'filename', 4)
    report.report_file_success(printer, analyzer, [], direntry, patloc, [])
    (out, err) = capsys.readouterr()
    assert out == ''
    assert err == ''


# report_success()

# These seem of questionable value, peeking overly into internals.

def test_report_success_dir(monkeypatch):
    '''Successfully report on a dir'''

    class Namespace():
        pass

    ns = Namespace()
    ns.called = False

    def mock_report_dir_success(printer, analyzer, ignores, headers,
                                direntry, patloc, report_items):
        ns.called = True
    monkeypatch.setattr(report, 'report_dir_success', mock_report_dir_success)

    class MockDirEntry():
        def is_dir(self):
            return True

    direntry = MockDirEntry()
    report.report_success(None, None, set(), [], direntry, set([None]), [])
    assert ns.called


def test_report_success_file(monkeypatch):
    '''Successfully report on a file'''

    class Namespace():
        pass

    ns = Namespace()
    ns.called = False

    def mock_report_file_success(
            printer, analyzer, headers, direntry, patloc, report_items):
        ns.called = True
    monkeypatch.setattr(
        report, 'report_file_success', mock_report_file_success)

    class MockDirEntry():
        def is_dir(self):
            return False

    direntry = MockDirEntry()
    report.report_success(None, None, set(), [], direntry, set([None]), [])
    assert ns.called


# unmatched_visual_pointer()

def test_unmatched_visual_pointer_string():
    '''Do we get the pointer we expect when the text is unmached?'''
    unmatched = ' words'
    name = 'some words'
    vptr = '    ^'
    assert report.unmatched_visual_pointer([], name, unmatched) == vptr


def test_unmatched_visual_pointer_true():
    '''Do we get the pointer we expect when the text is unmached?'''
    unmatched = True
    name = 'some words'
    vptr = '          ^'
    assert report.unmatched_visual_pointer([], name, unmatched) == vptr


# report_failure()

def test_report_failure(capsys):
    '''Deliver multi-line output'''

    printer = printout.Printer()
    stripped_name = 'filename'
    direntry = MockDirEntry(stripped_name)
    patloc = fstree.PatternLoc(3, 'file name', 4)
    report.report_failure(
        printer, [], direntry, stripped_name, True, set([patloc]))
    (out, err) = capsys.readouterr()
    assert out.count('\n') > 0
    assert err == ''


# strip_extension()

def test_strip_extension_dir():
    '''Does a dir keep its extension?'''
    name = 'dir.name'
    direntry = MockDirDirEntry(name)
    assert report.strip_extension(direntry) == name


def test_strip_extension_file():
    '''Does a file lose its extension?'''
    direntry = MockFileDirEntry('file.ext')
    assert report.strip_extension(direntry) == 'file'


# entry_report()

def test_entry_report_failure(capsys):
    '''Does a failure deliver multi-line output?'''
    class MockFSTree():
        def invalid(self, name):
            patloc = fstree.PatternLoc(3, 'dir name', 3)
            return (True, set([patloc]), [])

    printer = printout.Printer()
    analyzer = MockFSTree()
    direntry = MockDirDirEntry('dirname')
    report.entry_report(printer, analyzer, set(), [], direntry)
    (out, err) = capsys.readouterr()
    assert out.count('\n') > 0
    assert err == ''


def test_entry_report_success(monkeypatch):
    '''Is report_success() called when the analyzer matches?'''
    class Namespace():
        pass

    ns = Namespace()
    ns.called = False

    class MockFSTree():
        def invalid(self, name):
            return ('', set([]), [])

        def is_dir(self):
            return True

    def mock_report_success(printer, analyzer, ignores, headers,
                            direntry, patlocs, report_items):
        ns.called = True
    monkeypatch.setattr(
        report, 'report_success', mock_report_success)

    analyzer = MockFSTree()
    direntry = MockFileDirEntry('file name.ext')
    report.entry_report(None , analyzer, set(), [], direntry)
    assert ns.called


# dir_report

def test_dir_report_headers(monkeypatch):
    '''Headers don't change'''
    def mock_scandir(path):
        return set([])
    monkeypatch.setattr(report, 'scandir', mock_scandir)

    def mock_entry_report(printer, analyzer, ignores, headers, direntry):
        pass
    monkeypatch.setattr(report, 'entry_report', mock_entry_report)

    orig_headers = []
    headers = orig_headers[:]
    direntry = MockDirEntry('dirname')
    report.dir_report(None, None, set(), headers, direntry)

    assert headers == orig_headers


def test_dir_report_entries(monkeypatch):
    '''report_entry() is called with the entries of the dir,

    sorted case-insensitivly by name'''
    class Namespace():
        pass

    ns = Namespace()
    ns.names = []

    def mock_scandir(path):
        return set([
            MockDirEntry('xyz'),
            MockDirEntry('ABx'),
            MockDirEntry('Abc')])
    monkeypatch.setattr(report, 'scandir', mock_scandir)

    def mock_entry_report(printer, analyzer, ignores, headers, direntry):
        ns.names.append(direntry.name)
    monkeypatch.setattr(report, 'entry_report', mock_entry_report)

    direntry = MockDirEntry('dirname')
    report.dir_report(None, None, set(), [], direntry)

    assert ns.names == ['Abc', 'ABx', 'xyz']


def test_dir_report_ignores(monkeypatch):
    '''report_entry() is not called with ignored entries of the dir,

    sorted case-insensitivly by name'''
    class Namespace():
        pass

    ns = Namespace()
    ns.names = []

    def mock_scandir(path):
        return set([
            MockDirEntry('xyz'),
            MockDirEntry('ABx'),
            MockDirEntry('Abc')])
    monkeypatch.setattr(report, 'scandir', mock_scandir)

    def mock_entry_report(printer, analyzer, ignores, headers, direntry):
        ns.names.append(direntry.name)
    monkeypatch.setattr(report, 'entry_report', mock_entry_report)

    direntry = MockDirEntry('dirname')
    report.dir_report(None, None, set(['ABx', 'Abc']), [], direntry)

    assert ns.names == ['xyz']


# analyze_dirs()

def test_analyze_dirs_nodirs(capsys, monkeypatch):
    '''Print a message when there's nothing in the current directory'''
    def mock_scandir(path):
        return set([])
    monkeypatch.setattr(report, 'scandir', mock_scandir)

    report.analyze_dirs(printout.Printer(), None, set())
    (out, err) = capsys.readouterr()
    assert err == ''
    assert out


def test_analyze_dirs_sorted(monkeypatch):
    '''report_dir() is called with dir content sorted
    case-insentivitly by name'''
    class Namespace():
        pass

    ns = Namespace()
    ns.names = []

    def mock_scandir(path):
        return set([
            MockDirDirEntry('xyz'),
            MockDirDirEntry('ABx'),
            MockDirDirEntry('Abc')])
    monkeypatch.setattr(report, 'scandir', mock_scandir)

    def mock_dir_report(printer, analyzer, ignores, headers, direntry):
        ns.names.append(direntry.name)
    monkeypatch.setattr(report, 'dir_report', mock_dir_report)

    report.analyze_dirs(printout.Printer(), None, {'ignored_names': set()})
    assert ns.names == ['Abc', 'ABx', 'xyz']


def test_analyze_dirs_file(capsys, monkeypatch):
    '''Print a message when a file is checked as a directory'''
    def mock_scandir(path):
        return set([MockFileDirEntry('filename')])
    monkeypatch.setattr(report, 'scandir', mock_scandir)

    report.analyze_dirs(printout.Printer(), None, set())
    (out, err) = capsys.readouterr()
    assert err == ''
    assert out
