# 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 rules_yacc
from enforcer import parse_rules
from enforcer import scan_item


# Helper functions

def not_match(matchfunc, data):
    assert matchfunc(data) != (len(data), [])


def does_match(matchfunc, data):
    assert matchfunc(data) == (len(data), [])


#
# Class tests
#

# ScanItem
def test_scanitem_hashable():
    '''Can a ScanItem be used as a hashable?'''
    dct = {scan_item.ScanItem('foo', 0): 'bar'}
    assert scan_item.ScanItem('foo', 0) in dct


def test_scanitem_match():
    with pytest.raises(NotImplementedError):
        scan_item.ScanItem('foo', 0).matches('bar')


# ValuedScanItem
def test_valuedscanitem_hashable():
    '''Can a ScanItem be used as a hashable?'''
    dct = {scan_item.ValuedScanItem('foo', 0, 'bar'): 'baz'}
    assert scan_item.ValuedScanItem('foo', 0, 'bar') in dct


# UserStrScanItem
def test_userstrscanitem_matches():
    '''Does the match match a valid string?'''
    item = scan_item.UserStrScanItem(0)
    data = 'Some-99 String'
    does_match(item.matches, data)


def test_userstrscanitem_stops():
    '''Does the match stop after a valid string?'''
    item = scan_item.UserStrScanItem(0)
    data = 'Some-99 String'
    assert item.matches(data + ' - more Text') == (len(data), [])


# SpecialUserStrScanItem
def test_specialuserstrscanitem_notspecial():
    '''A SpecialUserStrScanItem compares different from a UserStrScanItem
    of the same type'''
    assert (scan_item.SpecialUserStrScanItem(3, '<element>', ' value ') !=
            scan_item.UserStrScanItem(3, '<element>'))


def test_specialuserstrscanitem_different():
    '''2 objects compare different when the strings following the <user_item>
    differs'''
    assert (scan_item.SpecialUserStrScanItem(3, '<element>', ' value0 ') !=
            scan_item.SpecialUserStrScanItem(3, '<element>', ' value1 '))


def test_specialuserstrscanitem_same():
    '''2 objects compare equal when the strings followig the <user_item>
    do not differ'''
    assert (scan_item.SpecialUserStrScanItem(3, '<element>', ' value ') ==
            scan_item.SpecialUserStrScanItem(3, '<element>', ' value '))


def test_specialuserstrscanitem_trail_succeed():
    '''Comparison works when there's a trailing string where expected'''
    item = scan_item.SpecialUserStrScanItem(3, '<element>', ' value ')
    data = 'Some Element or Another'
    assert item.matches(data + ' value and more') == (len(data), [])


def test_specialuserstrscanitem_notrail_succeed():
    '''Comparison works when there's no trailing string'''
    item = scan_item.SpecialUserStrScanItem(3, '<element>', ' value ')
    data = 'Some Element or Another'
    does_match(item.matches, data)


def test_specialuserstrscanitem_trail_fail():
    '''Comparison fails when there's a trailing string where not expected'''
    item = scan_item.SpecialUserStrScanItem(3, '<element>', ' value ')
    data = 'Some random value or Another'
    result = item.matches(data + ' value and more')
    assert result != (len(data), [])
    assert result[0] == len('Some random')


def test_specialuserstrscanitem_hash():
    '''The hash function works with special user strings'''
    item0 = scan_item.SpecialUserStrScanItem(3, '<element>', ' value0 ')
    item1 = scan_item.SpecialUserStrScanItem(3, '<element>', ' value1 ')
    assert hash(item0) == hash(item0)
    assert hash(item0) != hash(item1)


# HashScanItem
def test_hashscanitem_matches():
    '''A hash scan item matches digits'''
    item = scan_item.HashScanItem(0)
    data = '2340'
    does_match(item.matches, data)


def test_hashscanitem_matches_leading0():
    '''A hash scan item matches digits with leading zeros'''
    item = scan_item.HashScanItem(0)
    data = '02340'
    does_match(item.matches, data)


def test_hashscanitem_fails():
    '''A hash scan item does not match digits and letters'''
    item = scan_item.HashScanItem(0)
    data = '02a340'
    not_match(item.matches, data)


# EntityScanItem
def test_entity_scan_item():
    '''An entity matches a reasonable entity name'''
    item = scan_item.EntityScanItem(0)
    data = 'Some Entity'
    does_match(item.matches, data)


# DateScanItem
def test_date_scan_item_matches_gooddate():
    '''A date scan item matches a valid date'''
    item = scan_item.DateScanItem(0)
    data = '2017-01-01'
    does_match(item.matches, data)


def test_date_scan_item_matches_baddate():
    '''A date scan item matches an invalid date'''
    item = scan_item.DateScanItem(0)
    data = '0000-00-00'
    does_match(item.matches, data)


def test_date_scan_item_mismatch_short():
    '''A date scan item does not match a short date'''
    item = scan_item.DateScanItem(0)
    data = '2017-1-1'
    not_match(item.matches, data)


def test_date_scan_item_mismatch_letters():
    '''A date scan item does not match non-date-like string'''
    item = scan_item.DateScanItem(0)
    data = '2017-sandwitch-1'
    not_match(item.matches, data)


# YearScanItem
def test_year_scan_item_match_good():
    '''A year scan item matches a good year'''
    item = scan_item.YearScanItem(0)
    data = '2017'
    does_match(item.matches, data)


def test_year_scan_item_match_bad():
    '''A year scan item matches a bad year'''
    item = scan_item.YearScanItem(0)
    data = '0017'
    does_match(item.matches, data)


def test_year_scan_item_mismatch_alphs():
    '''A year scan item does not match alpha chars'''
    item = scan_item.YearScanItem(0)
    data = '20a17'
    not_match(item.matches, data)


# Last4DigitsScanItem
def test_last4digitsscanitem_matches():
    '''A hash scan item matches digits'''
    item = scan_item.Last4DigitsScanItem(0)
    data = '2340'
    does_match(item.matches, data)


def test_last4digitsscanitem_matches_leading0():
    '''A hash scan item matches digits with leading zeros'''
    item = scan_item.Last4DigitsScanItem(0)
    data = '0234'
    does_match(item.matches, data)


def test_last4digitsscanitem_fails():
    '''A hash scan item does not match digits and letters'''
    item = scan_item.Last4DigitsScanItem(0)
    data = '2a340'
    not_match(item.matches, data)


# VersionScanItem
def test_versionscanitem_matches():
    '''A version number matches'''
    item = scan_item.VersionScanItem(0)
    data = ' v10'
    does_match(item.matches, data)


def test_versionscanitem_fails_leading0():
    '''A version with a leading 0 fails'''
    item = scan_item.VersionScanItem(0)
    data = ' v01'
    not_match(item.matches, data)


# ParenScanItem
def test_parenscanitem_matches_plain():
    '''A parenthetical with no optional user text matches'''
    item = scan_item.ParenScanItem(0, ' (some parenthetical)')
    data = ' (some parenthetical)'
    does_match(item.matches, data)


def test_parenscanitem_matches_extra():
    '''A parenthetical with valid optional user text matches and returns
    the expected content to be reported'''
    item = scan_item.ParenScanItem(0, ' (some parenthetical)')
    content = 'some parenthetical; Extra Stuff'
    data = ' (' + content + ')'
    assert item.matches(data) == (len(data), [content])


def test_parenscanitem_nomatch_differ():
    '''A parenthetical with different content fails to match'''
    item = scan_item.ParenScanItem(0, ' (some parenthetical)')
    data = ' (some other parenthetical)'
    not_match(item.matches, data)


def test_parenscanitem_nomatch_extra():
    '''A parenthetical with invalid optional user text does not match'''
    item = scan_item.ParenScanItem(0, ' (some parenthetical)')
    data = ' (some parenthetical; ===)'
    not_match(item.matches, data)


def test_parenscanitem_nomatch_notparen():
    '''A parenthetical does not match non-parenthesis'''
    item = scan_item.ParenScanItem(0, ' (some parenthetical)')
    data = ' some parenthetical'
    not_match(item.matches, data)


# RepeatedParensScanItem
def test_repeatedparensscanitem_matches1():
    '''A repeated parens scan item matches a single parenthetical'''
    item = scan_item.RepeatedParensScanItem(0)
    content = 'some text and more'
    data = ' (' + content + ')'
    assert item.matches(data) == (len(data), [content])


def test_repeatedparensscanitem_matches2():
    '''A repeated parens scan item matches two parentheticals'''
    item = scan_item.RepeatedParensScanItem(0)
    content1 = 'some text and more'
    content2 = 'another parenthetical'
    data = ' (' + content1 + ') (' + content2 + ')'
    assert item.matches(data) == (len(data), [content1, content2])


def test_repeatedparensscanitem_fails_trailing():
    '''A repeated parens scan item fails on trailing chars'''
    item = scan_item.RepeatedParensScanItem(0)
    data = ' (some text; and more) '
    not_match(item.matches, data)


def test_repeatedparensscanitem_fails_notclosed():
    '''A repeated parens scan item fails when parenthetical is not closed'''
    item = scan_item.RepeatedParensScanItem(0)
    data = ' (some text; and more '
    not_match(item.matches, data)


def test_repeatedparensscanitem_fails_noleadingspace():
    '''A repeated parens scan item fails without a leading space'''
    item = scan_item.RepeatedParensScanItem(0)
    data = '(some text; and more)'
    not_match(item.matches, data)


def test_repeatedparensscanitem_fails_openspace():
    '''A repeated parens scan item fails when space follows open paren'''
    item = scan_item.RepeatedParensScanItem(0)
    data = '( some text; and more)'
    not_match(item.matches, data)


def test_repeatedparensscanitem_fails_closespace():
    '''A repeated parens scan item fails when space leads close paren'''
    item = scan_item.RepeatedParensScanItem(0)
    data = '(some text; and more )'
    not_match(item.matches, data)


def test_repeatedparensscanitem_fails_nocontent():
    '''A repeated parens scan item fails when empty'''
    item = scan_item.RepeatedParensScanItem(0)
    data = '()'
    not_match(item.matches, data)


# OptionalScanItem
def test_optionalscanitem_does_not_match():
    '''Trying to match an optional scan item raises an error'''
    item = scan_item.OptionalScanItem(0, [])
    with pytest.raises(NotImplementedError):
        item.matches('anything')


#
# Functions
#

# Helper functions

parse = rules_yacc.parser.parse


# Tests

def test_repeated_paren_content():
    '''Returns what's expected'''
    content1 = 'some text and more'
    content2 = 'another parenthetical'
    data = ' (' + content1 + ') (' + content2 + ')'
    assert scan_item.repeated_paren_content(data) == [content1, content2]


# scan_to_specialuserstrscanitem()
def test_scan_to_specialuserstrscanitem():
    '''Test that a scan item list of one item produces a
    specialuserstrscanitem with an empty string for a next_str'''
    # Note that this function is never called this way within
    # the running code, but we don't want to have to think about
    # what it means to omit the execution path tested here from
    # the test output.
    item = scan_item.scan_to_specialuserstrscanitem(
        [('user_str', '<anything>')], 0)
    assert item.next_str == ''


# tuple_to_items()
def test_tuple_to_items_entity():
    '''Does an entity tuple produce an entity scan item?'''
    items = scan_item.tuple_to_items([('entity', 'Some Entity')], 3)
    assert len(items) == 1
    item = items[0]
    assert item.type == 'entity'
    assert item.pos == 3


def test_tuple_to_items_badtuple():
    '''Do we get an exception when supplying a bad tuple?'''
    with pytest.raises(ValueError):
        scan_item.tuple_to_items([('unknowntype', 'somevalue')], 3)


def test_tuple_to_items_parsed():
    '''A speicaluserstrscanitem gets the correct next_str when given
    parsed input'''
    scan = parse_rules.parser.parse('<element one> by <element two>')
    items = scan_item.tuple_to_items(scan, 10)
    assert items[0].next_str == ' by '


# to_scan_item()
def test_to_scan_item_pos_simple():
    '''Do we get valid position information in scan items?'''
    #                                      01234567890
    items = scan_item.to_scan_items(parse('two words <address>'))
    assert items[0].pos == 0
    assert items[10].type == 'address'
    assert items[10].pos == 10


def test_to_scan_item_pos_complex():
    '''Do we get valid position information in scan items?'''
    items = scan_item.to_scan_items(
        #      01234567890123456789012
        parse('two words <address> - <yyyy-mm-dd>'))
    assert items[14].type == 'date'
    assert items[14].pos == 22


def test_to_scan_item_optional_pos():
    '''Is position information valid with optional pattern components?'''
    items = scan_item.to_scan_items(
        #      01234567890123456789012345678901234567890
        parse('two words <address>[ - <yyyy-mm-dd>][ - <last 4 digits>]'))

    assert items[11].type == 'optional'
    assert items[11].pos == 19

    assert items[11].value[0].type == 'string'
    assert items[11].value[0].pos == 20

    assert items[11].value[3].type == 'date'
    assert items[11].value[3].pos == 23

    assert items[12].type == 'optional'
    assert items[12].pos == 36

    assert items[12].value[3].type == 'last_4_digits'
    assert items[12].value[3].pos == 40
