import collections
from unittest.mock import Mock

import pytest
from pactman.verifier.matching_rule import (InvalidMatcher, MatchDecimal,
                                            MatchEquality, Matcher,
                                            MatchInclude, MatchInteger,
                                            MatchNull, MatchNumber, MatchRegex,
                                            MatchType, RuleFailed, fold_type,
                                            log, rule_matchers_v2,
                                            rule_matchers_v3, split_path,
                                            weight_path)


def test_stringify():
    r = MatchType('$', {'match': 'type'})
    assert str(r) == "Rule match by {'match': 'type'} at $"
    assert repr(r) == "<MatchType path='$' rule={'match': 'type'}>"


def test_invalid_match_type(monkeypatch):
    monkeypatch.setattr(log, 'warning', Mock())
    assert isinstance(Matcher.get_matcher('$', {'match': 'spam'}), InvalidMatcher)
    log.warning.assert_called_once()


@pytest.mark.parametrize('path, weight, spam_weight', [
    ('$.body', 4, 4),
    ('$.body.item1.level[0].id', 0, 0),
    ('$.body.item1.level[1].id', 64, 0),
    ('$.body.item1.level[*].id', 32, 0),
    ('$.body.*.level[*].id', 16, 0),
    ('$.body.*.*[*].id', 8, 8),
])
def test_weightings(path, weight, spam_weight):
    rule = Matcher(path, {'match': 'type'})
    assert rule.weight(['$', 'body', 'item1', 'level', 1, 'id']).weight == weight
    assert rule.weight(['$', 'body', 'item2', 'spam', 1, 'id']).weight == spam_weight


@pytest.mark.parametrize('path, result', [
    ('', []),
    ('$', ['$']),
    ('$.body', ['$', 'body']),
    ('$.body.item1', ['$', 'body', 'item1']),
    ('$.body.item2', ['$', 'body', 'item2']),
    ('$.header.item1', ['$', 'header', 'item1']),
    ('$.body.item1.level', ['$', 'body', 'item1', 'level']),
    ('$.body.item1.level[1]', ['$', 'body', 'item1', 'level', 1]),
    ('$.body.item1.level[1].id', ['$', 'body', 'item1', 'level', 1, 'id']),
    ('$.body.item1.level[1].name', ['$', 'body', 'item1', 'level', 1, 'name']),
    ('$.body.item1.level[2]', ['$', 'body', 'item1', 'level', 2]),
    ('$.body.item1.level[2].id', ['$', 'body', 'item1', 'level', 2, 'id']),
    ('$.body.item1.level[*].id', ['$', 'body', 'item1', 'level', '*', 'id']),
    ('$.body.*.level[*].id', ['$', 'body', '*', 'level', '*', 'id']),
])
def test_split_path(path, result):
    assert list(split_path(path)) == result


@pytest.mark.parametrize('spec, test, result', [
    (['a'], ['b'], 0),
    (['*'], ['a'], 1),
    (['a', '*'], ['a', 'b'], 2),
    (['a', 'b'], ['a', 'b'], 4),
    (['a', 'b', 'c'], ['a', 'b'], 0),
    (['a', 'b'], ['a', 'b', 'c'], 4),
])
def test_weight_path(spec, test, result):
    assert weight_path(spec, test) == result


@pytest.mark.parametrize('data, spec', [
    (1, 1),
    (1, 1.0),
    (1.0, 1.0),
    (1.0, 1.0),
])
def test_numbers(data, spec):
    MatchType('$', dict(match='type')).apply(data, spec, ['a'])


def test_regex():
    MatchRegex('$', dict(match='regex', regex=r'\w+')).apply('spam', None, ['a'])


def test_regex_fail():
    with pytest.raises(RuleFailed):
        MatchRegex('$', dict(match='regex', regex=r'\W+')).apply('spam', None, ['a'])


def test_integer():
    MatchInteger('$', dict(match='integer')).apply(1, None, ['a'])


def test_integer_fail():
    with pytest.raises(RuleFailed):
        MatchInteger('$', dict(match='integer')).apply(1.0, None, ['a'])


def test_decimal():
    MatchDecimal('$', dict(match='decimal')).apply(1.0, None, ['a'])


def test_decimal_fail():
    with pytest.raises(RuleFailed):
        MatchDecimal('$', dict(match='decimal')).apply(1, None, ['a'])


@pytest.mark.parametrize('value', [1, 1.0])
def test_number(value):
    MatchNumber('$', dict(match='number')).apply(value, None, ['a'])


def test_number_fail():
    with pytest.raises(RuleFailed):
        MatchNumber('$', dict(match='number')).apply('spam', None, ['a'])


def test_equality():
    MatchEquality('$', dict(match='equality')).apply('spam', 'spam', ['a'])


def test_equality_fail():
    with pytest.raises(RuleFailed):
        MatchEquality('$', dict(match='equality')).apply('ham', 'spam', ['a'])


def test_include():
    MatchInclude('$', dict(match='include', value='spam')).apply('spammer', None, ['a'])


def test_include_fail():
    with pytest.raises(RuleFailed):
        MatchInclude('$', dict(match='include', value='spam')).apply('ham', None, ['a'])


def test_null():
    MatchNull('$', dict(match='null')).apply(None, None, ['a'])


def test_null_fail():
    with pytest.raises(RuleFailed):
        MatchNull('$', dict(match='null')).apply('ham', None, ['spam'])


def test_min():
    MatchType('$', dict(match='type', min=1)).apply(['spam'], ['a'], [])


def test_min_not_met():
    with pytest.raises(RuleFailed):
        MatchType('$', dict(match='type', min=2)).apply(['spam'], ['a', 'b'], ['spam'])


def test_min_ignored():
    MatchType('$', dict(match='type', min=1)).apply(0, 0, [])


def test_max():
    MatchType('$', dict(match='type', max=1)).apply(['spam'], ['a'], [])


def test_max_not_met():
    with pytest.raises(RuleFailed):
        MatchType('$', dict(match='type', max=2)).apply([1, 2, 3], [1, 2], ['spam'])


def test_max_ignored():
    MatchType('$', dict(match='type', max=1)).apply(0, 0, [])


@pytest.mark.parametrize('source, result', [
    ({}, dict),
    ([], list),
    (collections.OrderedDict(), dict),
])
def test_fold_type(source, result):
    assert fold_type(source) == result


def test_build_matching_rules_handles_rule_with_unknown_type_v2(monkeypatch):
    monkeypatch.setattr(log, 'warning', Mock())
    rules = rule_matchers_v2({
        "$.body": {"match": "SPAM"},
        "$.body[*].*": {"match": "type"}
    })
    assert 2 == len(rules['body'])
    log.warning.assert_called_once()


def test_build_matching_rules_handles_rule_with_unknown_type_v3(monkeypatch):
    monkeypatch.setattr(log, 'warning', Mock())
    rules = rule_matchers_v3({
        "body": {
            "$": {"matchers": [{"match": "SPAM"}]},
            "$[*].*": {"matchers": [{"match": "type"}]}
        }
    })
    assert 2 == len(rules['body'])
    log.warning.assert_called_once()
