# 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 pytest

from enforcer import exceptions as ex
from enforcer import fstree
from enforcer import parse_rules
from enforcer import startup

import libtest


#
# Helper functions for class testing
#

def patlocs_to_tuples(pls):
    '''Return a set of (rownum, pos) tuples from the PatternLocs supplied'''
    return set([(pl.rownum, pl.pos) for pl in pls])


#
# Function tests
#
def test_breakon_version_parens_plain():
    '''Does breaking a string with no version an no parens work?'''
    assert fstree.breakon_version_parens('word') == ['word']


def test_breakon_version_parens_version():
    '''Does breaking a string with a version number work?'''
    assert fstree.breakon_version_parens('word v5') == ['word', ' v5']


def test_breakon_version_parens_version_paren():
    '''Does breaking a string with a version number and paren work?'''
    assert (fstree.breakon_version_parens('word v5 (paren)') ==
            ['word', ' v5 (paren)'])


def test_breakon_version_parens_parens():
    '''Does breaking a string with no version number and parens work?'''
    assert (fstree.breakon_version_parens('word (paren) (again)') ==
            ['word', ' (paren) (again)'])


def test_reassemble():
    '''Does re-assembly do the right thing?'''
    assert fstree.reassemble('abc', ['def']) == 'abcdef'


#
# Class tests
#

# FSDir
def test_fsdir_is_root_true():
    '''Can we set and detect the root node?'''
    fsdir = fstree.FSDir(fstree.DecisionTree, 'pattern')
    fsdir.set_root()
    assert fsdir.is_root()


def test_fsdir_is_root_false():
    '''Are non-root nodes detected as such?'''
    fsdir = fstree.FSDir(fstree.DecisionTree, 'pattern')
    assert not fsdir.is_root()


def test_fsdir_add_child():
    '''Are child nodes retained as expected?'''
    fsdir = fstree.FSDir(fstree.DecisionTree, 'pattern')
    fsdir.add_child(3, 'first')
    fsdir.add_child(4, 'second')
    fsdir.add_child(5, 'third')
    assert len(fsdir.children) == 3


def test_fsdir_get_patterns():
    '''Is the list of directory content added and returned as expected?'''
    fsdir = fstree.FSDir(libtest.MockTree, 'pattern')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 1, 'first')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 2, 'second')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 3, 'third')
    assert (fsdir.decision_tree.tree ==
            [(1, 'first'), (2, 'second'), (3, 'third')])


def test_fsdir_invalid_match():
    '''Does a simple string match work?'''
    fsdir = fstree.FSDir(fstree.DecisionTree, '.')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 1, 'first')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 2, 'second')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 3, 'third')
    (unmatched, patlocs, report_items) = fsdir.invalid('second')
    assert unmatched == ''
    patloc = patlocs.pop()
    assert len(patlocs) == 0
    assert patloc.rownum == 2
    assert patloc.pos == 6
    assert report_items == []


def test_fsdir_scan_error():
    '''Are lexing errors detected?'''
    fsdir = fstree.FSDir(fstree.DecisionTree, 'dir pattern')
    with pytest.raises(ex.ScanError):
        fsdir.add_content(fsdir.decision_tree, parse_rules.parser,
                          1, 'first(problem)')


def test_fsdir_parse_error():
    '''Are yacc bad-token errors detected?'''
    fsdir = fstree.FSDir(fstree.DecisionTree, 'dir pattern')
    with pytest.raises(ex.ParseError):
        fsdir.add_content(fsdir.decision_tree, parse_rules.parser,
                          1, 'first<problem>')


def test_fsdir_eof_error():
    '''Are yacc eof errors detected?'''
    fsdir = fstree.FSDir(fstree.DecisionTree, 'dir pattern')
    with pytest.raises(ex.ParseEOFError):
        fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 1, 'first ')


# DecisionNode

# Although DecisionNodes are lower-level than FSDirs, we test them
# later because it's convenient to use a FSDir to do the testing.
def test_dn_invalid_match():
    '''Does a simple string match work?'''
    fsdir = fstree.FSDir(fstree.DecisionTree, '.')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 1, 'first')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 2, 'second')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 3, 'third')
    (unmatched, patlocs, report_items) = fsdir.decision_tree.invalid('second')
    assert unmatched == ''
    patloc = patlocs.pop()
    assert len(patlocs) == 0
    assert patloc.rownum == 2
    assert patloc.pos == 6
    assert report_items == []


def test_dn_invalid_match_1short():
    '''Does a simple string match fail when 1 char short?'''
    fsdir = fstree.FSDir(fstree.DecisionTree, '.')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 1, 'first')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 2, 'second')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 3, 'third')
    (unmatched, patlocs, report_items) = fsdir.decision_tree.invalid('secon')
    assert unmatched is True
    patloc = patlocs.pop()
    assert len(patlocs) == 0
    assert patloc.rownum == 2
    assert patloc.pos == 5
    assert report_items == []


def test_dn_invalid_match_intree():
    '''Does a string match work when multiple patterns partially match?'''
    fsdir = fstree.FSDir(fstree.DecisionTree, '.')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 1, 'something')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 2, 'second')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 3, 'third')
    (unmatched, patlocs, report_items) = fsdir.decision_tree.invalid('second')
    assert unmatched == ''
    patloc = patlocs.pop()
    assert len(patlocs) == 0
    assert patloc.rownum == 2
    assert patloc.pos == 6
    assert report_items == []


def test_dn_invalid_match_partial():
    '''Does a partial string match return expected results?'''
    fsdir = fstree.FSDir(fstree.DecisionTree, '.')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 1, 'something')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 2, 'second')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 3, 'third')
    (unmatched, patlocs, report_items) = fsdir.decision_tree.invalid('sec')
    assert unmatched is True
    patloc = patlocs.pop()
    assert len(patlocs) == 0
    assert patloc.rownum == 2
    assert patloc.pos == 3
    assert report_items == []


def test_dn_invalid_match_partial_multiple():
    '''Partial string matching multiple patterns returns expected results?'''
    fsdir = fstree.FSDir(fstree.DecisionTree, '.')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 1, 'something')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 2, 'second')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 3, 'third')
    (unmatched, patlocs, report_items) = fsdir.decision_tree.invalid('s')
    assert unmatched is True
    assert len(patlocs) == 2
    tuples = patlocs_to_tuples(patlocs)
    assert (1, 1) in tuples
    assert (2, 1) in tuples
    assert report_items == []


def test_dn_invalid_match_toolong():
    '''Failure to match because the name is too long'''
    fsdir = fstree.FSDir(fstree.DecisionTree, '.')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 1, 'something')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 2, 'second')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 3, 'third')
    #                                                   0123456789
    (unmatched, patlocs, report_items) = fsdir.decision_tree.invalid(
        'something else')
    assert unmatched == ' else'
    assert len(patlocs) == 1
    patloc = patlocs.pop()
    assert patloc.rownum == 1
    assert patloc.pos == 9
    assert report_items == []


def test_dn_invalid_match_same_pattern():
    '''Fail to match optional components puts failures at right spots'''
    fsdir = fstree.FSDir(fstree.DecisionTree, '.')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser
                      #     012345678901234567890123
                      , 1, 'word[ - <#>][ - <yyyy>]')
    (unmatched, patlocs, report_items) = fsdir.decision_tree.invalid(
        'word - a')
    assert unmatched == 'a'
    assert len(patlocs) == 2
    tuples = patlocs_to_tuples(patlocs)
    assert (1, 8) in tuples
    assert (1, 16) in tuples
    assert report_items == []


def test_dn_invalid_match_version():
    '''Matches a version number after a string'''
    fsdir = fstree.FSDir(fstree.DecisionTree, '.')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser
                      #     0123456789
                      , 1, 'word v<#>')
    (unmatched, patlocs, report_items) = fsdir.decision_tree.invalid('word v5')
    assert unmatched == ''
    patloc = patlocs.pop()
    assert len(patlocs) == 0
    assert patloc.rownum == 1
    assert patloc.pos == 9
    assert report_items == []


def test_dn_invalid_match_version_userstr():
    '''Matches a version number after a user element'''
    fsdir = fstree.FSDir(fstree.DecisionTree, '.')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser
                      #     0123456789012345678901
                      , 1, 'word - <address> v<#>')
    (unmatched, patlocs, report_items) = fsdir.decision_tree.invalid(
        'word - Park Place v5')
    assert unmatched == ''
    patloc = patlocs.pop()
    assert len(patlocs) == 0
    assert patloc.rownum == 1
    assert patloc.pos == 21
    assert report_items == []


def test_dn_invalid_match_implied_version_userstr():
    '''Matches a version number after a user element'''
    fsdir = fstree.FSDir(fstree.DecisionTree, '.')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser
                      #     01234567890123456
                      , 1, 'word - <address>')
    (unmatched, patlocs, report_items) = fsdir.decision_tree.invalid(
        'word - Park Place v5')
    assert unmatched == ''
    patloc = patlocs.pop()
    assert len(patlocs) == 0
    assert patloc.rownum == 1
    assert patloc.pos == 16
    assert report_items == []


def test_dn_invalid_match_implied_version_userstr_optparen():
    '''Matches a version number after a user element followed by an optional
    parenthetical'''
    fsdir = fstree.FSDir(fstree.DecisionTree, '.')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser
                      #     012345678901234567890123456789
                      , 1, 'word - <address>[ (optional)]')
    (unmatched, patlocs, report_items) = fsdir.decision_tree.invalid(
        'word - Park Place v5')
    assert unmatched == ''
    patloc = patlocs.pop()
    assert len(patlocs) == 0
    assert patloc.rownum == 1
    assert patloc.pos == 29
    assert report_items == []


def test_dn_invalid_match_parens():
    '''Matches optional parens after required parens'''
    fsdir = fstree.FSDir(fstree.DecisionTree, '.')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser
                      #     012345678901234567890123456
                      , 1, 'word - <address> (comment)')
    (unmatched, patlocs, report_items) = fsdir.decision_tree.invalid(
        'word - Park Place (comment) (extra paren)')
    assert unmatched == ''
    patloc = patlocs.pop()
    assert len(patlocs) == 0
    assert patloc.rownum == 1
    assert patloc.pos == 26
    assert report_items == ['extra paren']


def test_dn_match_nothing():
    '''Failure to match any pattern'''
    fsdir = fstree.FSDir(fstree.DecisionTree, '.')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 1, 'something')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 2, 'second')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser, 3, 'third')
    #                                                   0123456789
    (unmatched, patlocs, report_items) = fsdir.decision_tree.invalid('xxx')
    assert unmatched == 'xxx'
    assert len(patlocs) == 3
    assert [pl.pos for pl in patlocs] == [0, 0, 0]
    assert report_items == []


def test_dn_warn_no_option1(capsys):
    '''Do 2 rules where the 2nd is an extension of the first make a warning?'''
    startup.em.exit_if_errored()   # Is global state good? (!)
    root_dir = fstree.FSDir(fstree.DecisionTree, name_pattern='.')
    root_dir.add_content(root_dir.decision_tree,
                         parse_rules.parser,
                         2, 'one')
    root_dir.add_content(root_dir.decision_tree,
                         parse_rules.parser,
                         3, 'oneplus')
    (out, err) = capsys.readouterr()
    assert err != ''       # Got something on stderr
    startup.em.exit_if_errored()


def test_dn_warn_no_option2(capsys):
    '''Do 2 rules where the 1st is an extension of the 2nd make a warning?'''
    startup.em.exit_if_errored()   # Is global state good? (!)
    root_dir = fstree.FSDir(fstree.DecisionTree, name_pattern='.')
    root_dir.add_content(root_dir.decision_tree,
                         parse_rules.parser,
                         2, 'oneplus')
    root_dir.add_content(root_dir.decision_tree,
                         parse_rules.parser,
                         3, 'one')
    (out, err) = capsys.readouterr()
    assert err != ''       # Got something on stderr
    startup.em.exit_if_errored()


def test_dn_warn_dups(capsys):
    '''Do 2 idential rules make a warning?'''
    startup.em.exit_if_errored()   # Global state is good (!)
    root_dir = fstree.FSDir(fstree.DecisionTree, name_pattern='.')
    root_dir.add_content(root_dir.decision_tree,
                         parse_rules.parser,
                         2, 'one')
    root_dir.add_content(root_dir.decision_tree,
                         parse_rules.parser,
                         3, 'one')
    (out, err) = capsys.readouterr()
    assert err != ''       # Got something on stderr
    startup.em.exit_if_errored()


def test_dn_no_warn(capsys):
    '''Different rules should not make a warning'''
    root_dir = fstree.FSDir(fstree.DecisionTree, name_pattern='.')
    root_dir.add_content(root_dir.decision_tree,
                         parse_rules.parser,
                         2, 'oneA')
    root_dir.add_content(root_dir.decision_tree,
                         parse_rules.parser,
                         3, 'oneB')
    (out, err) = capsys.readouterr()
    assert err == ''      # Got nothing on stderr


# DecisionTree

def test_dt_invalid_report_items():
    '''Reported parens and optional parens are returned,
    in the correct order'''
    fsdir = fstree.FSDir(fstree.DecisionTree, '.')
    fsdir.add_content(fsdir.decision_tree, parse_rules.parser
                      #     012345678901234567890123456
                      , 1, 'word - <address> (comment)')
    matched_comment = 'comment; with extra'
    extra_paren1 = 'extra paren'
    extra_paren2 = 'another extra paren'
    (unmatched, patlocs, report_items) = fsdir.decision_tree.invalid(
        'word - Park Place (' + matched_comment + ') (' +
        extra_paren1 + ') (' + extra_paren2 + ')')
    assert unmatched == ''
    patloc = patlocs.pop()
    assert len(patlocs) == 0
    assert patloc.rownum == 1
    assert patloc.pos == 26
    assert report_items == [matched_comment, extra_paren1, extra_paren2]
