# Copyright 2013-2020 Ben Lambert

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

"""
Unit tests for edit_distance.
"""
import unittest

from edit_distance import edit_distance_backpointer
from edit_distance import edit_distance
from edit_distance import highest_match_action
from edit_distance import SequenceMatcher


class TestEditDistance(unittest.TestCase):
    """Class to hold all the tests for this package."""

    def test_edit_distance0(self):
        """Test edit distance between 'ab' and 'dab'."""
        a = ["a", "b"]
        b = ["d", "a", "b"]
        self.assertEqual(edit_distance(a, b), (1, 2))
        bp_expected_result = (
            1,
            2,
            [["insert", 0, 0, 0, 1], ["equal", 0, 1, 1, 2], ["equal", 1, 2, 2, 3]],
        )
        self.assertEqual(edit_distance_backpointer(a, b), bp_expected_result)

    def test_edit_distance1(self):
        """Test edit distance between 'ab' and 'acdab'."""
        a = ["a", "b"]
        b = ["a", "c", "d", "a", "b"]
        self.assertEqual(edit_distance(a, b), (3, 2))
        bp_expected_result = (
            3,
            2,
            [
                ["insert", 0, 0, 0, 1],
                ["insert", 0, 0, 1, 2],
                ["insert", 0, 0, 2, 3],
                ["equal", 0, 1, 3, 4],
                ["equal", 1, 2, 4, 5],
            ],
        )
        self.assertEqual(edit_distance_backpointer(a, b), bp_expected_result)

    def test_edit_distance2(self):
        """Test edit distance for 'hi my name is andy'."""
        a = ["hi", "my", "name", "is", "andy"]
        b = ["hi", "i'm", "my", "name's", "sandy"]
        self.assertEqual(edit_distance(a, b), (4, 1))
        bp_expected_result = (
            4,
            1,
            [
                ["equal", 0, 1, 0, 1],
                ["replace", 1, 2, 1, 2],
                ["replace", 2, 3, 2, 3],
                ["replace", 3, 4, 3, 4],
                ["replace", 4, 5, 4, 5],
            ],
        )
        self.assertEqual(edit_distance_backpointer(a, b), bp_expected_result)

    def test_edit_distance_highest_match(self):
        """Test edit distance for 'hi my name is andy', maximizing matches rather than
        minimizing edits."""
        a = ["hi", "my", "name", "is", "andy"]
        b = ["hi", "i'm", "my", "name's", "sandy"]
        self.assertEqual(
            edit_distance(a, b, action_function=highest_match_action), (4, 2)
        )
        bp_expected_result = (
            4,
            2,
            [
                ["equal", 0, 1, 0, 1],
                ["insert", 1, 1, 1, 2],
                ["equal", 1, 2, 2, 3],
                ["delete", 2, 3, 2, 2],
                ["replace", 3, 4, 3, 4],
                ["replace", 4, 5, 4, 5],
            ],
        )
        self.assertEqual(
            edit_distance_backpointer(a, b, action_function=highest_match_action),
            bp_expected_result,
        )

    def test_edit_distance3(self):
        """Test for 'are you at work now'."""
        a = ["are", "you", "at", "work", "now"]
        b = ["i", "feel", "are", "saying"]
        bp_expected_result = (
            5,
            0,
            [
                ["delete", 0, 1, 0, 0],
                ["replace", 1, 2, 0, 1],
                ["replace", 2, 3, 1, 2],
                ["replace", 3, 4, 2, 3],
                ["replace", 4, 5, 3, 4],
            ],
        )
        self.assertEqual(edit_distance_backpointer(a, b), bp_expected_result)

    def test_edit_distance4(self):
        """Test edit distance against an empty list."""
        a = []
        b = ["a", "c"]
        self.assertEqual(edit_distance(a, b), (2, 0))
        self.assertEqual(edit_distance(b, a), (2, 0))
        self.assertEqual(edit_distance(a, a), (0, 0))

    def test_sequence_matcher(self):
        """Test the sequence matcher."""
        a = ["a", "b"]
        b = ["a", "b", "d", "c"]
        sm = SequenceMatcher(a=a, b=b)
        opcodes = [
            ["equal", 0, 1, 0, 1],
            ["equal", 1, 2, 1, 2],
            ["insert", 2, 2, 2, 3],
            ["insert", 2, 2, 3, 4],
        ]
        self.assertEqual(sm.distance(), 2)
        self.assertEqual(sm.ratio(), 2 / 3)
        self.assertEqual(sm.quick_ratio(), 2 / 3)
        self.assertEqual(sm.real_quick_ratio(), 2 / 3)
        self.assertEqual(sm.distance(), 2)
        # This doesn't return anything, saves the value in the sm cache.
        self.assertTrue(not sm._compute_distance_fast())
        self.assertEqual(sm.get_opcodes(), opcodes)
        self.assertEqual(list(sm.get_matching_blocks()), [[0, 0, 1], [1, 1, 1]])

    def test_sequence_matcher2(self):
        """Test the sequence matcher."""
        a = ["a", "b"]
        b = ["a", "b", "d", "c"]
        sm = SequenceMatcher()
        sm.set_seq1(a)
        sm.set_seq2(b)
        self.assertEqual(sm.distance(), 2)
        sm.set_seqs(b, a)
        self.assertEqual(sm.distance(), 2)

    def test_unsupported(self):
        """
        Test if calling unimplemented methods actually generates an error.
        """
        a = ["a", "b"]
        b = ["a", "b", "d", "c"]
        sm = SequenceMatcher(a=a, b=b)
        with self.assertRaises(NotImplementedError):
            sm.find_longest_match(1, 2, 3, 4)
        with self.assertRaises(NotImplementedError):
            sm.get_grouped_opcodes()

    def test_issue4_simpler(self):
        """Test for error reported here:
        https://github.com/belambert/edit-distance/issues/4"""
        a = ["that", "continuous", "sanction", ":=", "("]
        b = ["continuous", ":=", "(", "sanction", "^"]
        sm = SequenceMatcher(a=a, b=b)
        self.assertEqual(sm.distance(), 4)
        target_opcodes = [
            ["delete", 0, 1, 0, 0],
            ["equal", 1, 2, 0, 1],
            ["delete", 2, 3, 0, 0],
            ["equal", 3, 4, 1, 2],
            ["equal", 4, 5, 2, 3],
            ["insert", 5, 5, 3, 4],
            ["insert", 5, 5, 4, 5],
        ]
        self.assertEqual(sm.get_opcodes(), target_opcodes)

    def test_issue4(self):
        """Test for error reported here:
        https://github.com/belambert/edit-distance/issues/4"""
        a = [
            "that",
            "continuous",
            "sanction",
            ":=",
            "(",
            "flee",
            "U",
            "complain",
            ")",
            "E",
            "attendance",
            "eye",
            "^",
            "flowery",
            "revelation",
            "^",
            "ridiculous",
            "destination",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
        ]  # noqa
        b = [
            "continuous",
            ":=",
            "(",
            "sanction",
            "^",
            "flee",
            "^",
            "attendance",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
            "<EOS>",
        ]  # noqa
        target_opcodes = [
            ["delete", 0, 1, 0, 0],
            ["equal", 1, 2, 0, 1],
            ["delete", 2, 3, 0, 0],
            ["equal", 3, 4, 1, 2],
            ["equal", 4, 5, 2, 3],
            ["insert", 5, 5, 3, 4],
            ["insert", 5, 5, 4, 5],
            ["equal", 5, 6, 5, 6],
            ["replace", 6, 7, 6, 7],
            ["replace", 7, 8, 7, 8],
            ["replace", 8, 9, 8, 9],
            ["replace", 9, 10, 9, 10],
            ["replace", 10, 11, 10, 11],
            ["replace", 11, 12, 11, 12],
            ["replace", 12, 13, 12, 13],
            ["replace", 13, 14, 13, 14],
            ["replace", 14, 15, 14, 15],
            ["replace", 15, 16, 15, 16],
            ["replace", 16, 17, 16, 17],
            ["replace", 17, 18, 17, 18],
            ["equal", 18, 19, 18, 19],
            ["equal", 19, 20, 19, 20],
            ["equal", 20, 21, 20, 21],
            ["equal", 21, 22, 21, 22],
            ["equal", 22, 23, 22, 23],
            ["equal", 23, 24, 23, 24],
            ["equal", 24, 25, 24, 25],
            ["equal", 25, 26, 25, 26],
            ["equal", 26, 27, 26, 27],
            ["equal", 27, 28, 27, 28],
            ["equal", 28, 29, 28, 29],
        ]  # noqa
        sm = SequenceMatcher(a=a, b=b)
        self.assertEqual(sm.distance(), 16)
        self.assertEqual(sm.get_opcodes(), target_opcodes)

    def test_issue13(self):
        sm = SequenceMatcher(a="abc", b="abdc")
        self.assertEqual(
            [
                ["equal", 0, 1, 0, 1],
                ["equal", 1, 2, 1, 2],
                ["insert", 2, 2, 2, 3],
                ["equal", 2, 3, 3, 4],
            ],
            sm.get_opcodes(),
        )
