# Copyright (C) 2005, 2006, 2007 Canonical Ltd
# Copyright (C) 2021-2023 Jelmer Vernooĳ <jelmer@jelmer.uk>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

import difflib
import os
import shutil
import tempfile
import unittest
from typing import Any, Callable, List, Sequence, Tuple, Type

import patiencediff

from . import _patiencediff_py


class TestPatienceDiffLib(unittest.TestCase):
    def setUp(self) -> None:
        super().setUp()
        self._unique_lcs: Callable[..., List[Tuple[int, int]]] = (
            _patiencediff_py.unique_lcs_py
        )
        self._recurse_matches: Callable[..., None] = (
            _patiencediff_py.recurse_matches_py
        )
        self._PatienceSequenceMatcher: Type[difflib.SequenceMatcher] = (
            _patiencediff_py.PatienceSequenceMatcher_py
        )

    def test_diff_unicode_string(self) -> None:
        a = "".join([chr(i) for i in range(4000, 4500, 3)])
        b = "".join([chr(i) for i in range(4300, 4800, 2)])
        sm = self._PatienceSequenceMatcher(None, a, b)
        mb = sm.get_matching_blocks()
        self.assertEqual(35, len(mb))

    def test_unique_lcs(self) -> None:
        unique_lcs = self._unique_lcs
        self.assertEqual(unique_lcs("", ""), [])
        self.assertEqual(unique_lcs("", "a"), [])
        self.assertEqual(unique_lcs("a", ""), [])
        self.assertEqual(unique_lcs("a", "a"), [(0, 0)])
        self.assertEqual(unique_lcs("a", "b"), [])
        self.assertEqual(unique_lcs("ab", "ab"), [(0, 0), (1, 1)])
        self.assertEqual(
            unique_lcs("abcde", "cdeab"), [(2, 0), (3, 1), (4, 2)]
        )
        self.assertEqual(
            unique_lcs("cdeab", "abcde"), [(0, 2), (1, 3), (2, 4)]
        )
        self.assertEqual(
            unique_lcs("abXde", "abYde"), [(0, 0), (1, 1), (3, 3), (4, 4)]
        )
        self.assertEqual(unique_lcs("acbac", "abc"), [(2, 1)])

    def test_recurse_matches(self) -> None:
        def test_one(
            a: Sequence[Any], b: Sequence[Any], matches: List[Tuple[int, int]]
        ) -> None:
            test_matches: List[Tuple[int, int]] = []
            self._recurse_matches(a, b, 0, 0, len(a), len(b), test_matches, 10)
            self.assertEqual(test_matches, matches)

        test_one(
            ["a", "", "b", "", "c"],
            ["a", "a", "b", "c", "c"],
            [(0, 0), (2, 2), (4, 4)],
        )
        test_one(
            ["a", "c", "b", "a", "c"],
            ["a", "b", "c"],
            [(0, 0), (2, 1), (4, 2)],
        )
        # Even though 'bc' is not unique globally, and is surrounded by
        # non-matching lines, we should still match, because they are locally
        # unique
        test_one(
            "abcdbce",
            "afbcgdbce",
            [(0, 0), (1, 2), (2, 3), (3, 5), (4, 6), (5, 7), (6, 8)],
        )

        # recurse_matches doesn't match non-unique
        # lines surrounded by bogus text.
        # The update has been done in patiencediff.SequenceMatcher instead

        # This is what it could be
        # test_one('aBccDe', 'abccde', [(0,0), (2,2), (3,3), (5,5)])

        # This is what it currently gives:
        test_one("aBccDe", "abccde", [(0, 0), (5, 5)])

    def assertDiffBlocks(
        self,
        a: Sequence[Any],
        b: Sequence[Any],
        expected_blocks: List[Tuple[int, int, int]],
    ) -> None:
        """Check that the sequence matcher returns the correct blocks.

        :param a: A sequence to match
        :param b: Another sequence to match
        :param expected_blocks: The expected output, not including the final
            matching block (len(a), len(b), 0)
        """
        matcher = self._PatienceSequenceMatcher(None, a, b)
        blocks = matcher.get_matching_blocks()
        last = blocks.pop()
        self.assertEqual((len(a), len(b), 0), last)
        self.assertEqual(expected_blocks, blocks)

    def test_matching_blocks(self) -> None:
        # Some basic matching tests
        self.assertDiffBlocks("", "", [])
        self.assertDiffBlocks([], [], [])
        self.assertDiffBlocks("abc", "", [])
        self.assertDiffBlocks("", "abc", [])
        self.assertDiffBlocks("abcd", "abcd", [(0, 0, 4)])
        self.assertDiffBlocks("abcd", "abce", [(0, 0, 3)])
        self.assertDiffBlocks("eabc", "abce", [(1, 0, 3)])
        self.assertDiffBlocks("eabce", "abce", [(1, 0, 4)])
        self.assertDiffBlocks("abcde", "abXde", [(0, 0, 2), (3, 3, 2)])
        self.assertDiffBlocks("abcde", "abXYZde", [(0, 0, 2), (3, 5, 2)])
        self.assertDiffBlocks("abde", "abXYZde", [(0, 0, 2), (2, 5, 2)])
        # This may check too much, but it checks to see that
        # a copied block stays attached to the previous section,
        # not the later one.
        # difflib would tend to grab the trailing longest match
        # which would make the diff not look right
        self.assertDiffBlocks(
            "abcdefghijklmnop",
            "abcdefxydefghijklmnop",
            [(0, 0, 6), (6, 11, 10)],
        )

        # make sure it supports passing in lists
        self.assertDiffBlocks(
            ["hello there\n", "world\n", "how are you today?\n"],
            ["hello there\n", "how are you today?\n"],
            [(0, 0, 1), (2, 1, 1)],
        )

        # non unique lines surrounded by non-matching lines
        # won't be found
        self.assertDiffBlocks("aBccDe", "abccde", [(0, 0, 1), (5, 5, 1)])

        # But they only need to be locally unique
        self.assertDiffBlocks(
            "aBcDec", "abcdec", [(0, 0, 1), (2, 2, 1), (4, 4, 2)]
        )

        # non unique blocks won't be matched
        self.assertDiffBlocks("aBcdEcdFg", "abcdecdfg", [(0, 0, 1), (8, 8, 1)])

        # but locally unique ones will
        self.assertDiffBlocks(
            "aBcdEeXcdFg",
            "abcdecdfg",
            [(0, 0, 1), (2, 2, 2), (5, 4, 1), (7, 5, 2), (10, 8, 1)],
        )

        self.assertDiffBlocks("abbabbXd", "cabbabxd", [(7, 7, 1)])
        self.assertDiffBlocks("abbabbbb", "cabbabbc", [])
        self.assertDiffBlocks("bbbbbbbb", "cbbbbbbc", [])

    def test_matching_blocks_tuples(self) -> None:
        # Some basic matching tests
        self.assertDiffBlocks([], [], [])
        self.assertDiffBlocks([("a",), ("b",), ("c,")], [], [])
        self.assertDiffBlocks([], [("a",), ("b",), ("c,")], [])
        self.assertDiffBlocks(
            [("a",), ("b",), ("c,")], [("a",), ("b",), ("c,")], [(0, 0, 3)]
        )
        self.assertDiffBlocks(
            [("a",), ("b",), ("c,")], [("a",), ("b",), ("d,")], [(0, 0, 2)]
        )
        self.assertDiffBlocks(
            [("d",), ("b",), ("c,")], [("a",), ("b",), ("c,")], [(1, 1, 2)]
        )
        self.assertDiffBlocks(
            [("d",), ("a",), ("b",), ("c,")],
            [("a",), ("b",), ("c,")],
            [(1, 0, 3)],
        )
        self.assertDiffBlocks(
            [("a", "b"), ("c", "d"), ("e", "f")],
            [("a", "b"), ("c", "X"), ("e", "f")],
            [(0, 0, 1), (2, 2, 1)],
        )
        self.assertDiffBlocks(
            [("a", "b"), ("c", "d"), ("e", "f")],
            [("a", "b"), ("c", "dX"), ("e", "f")],
            [(0, 0, 1), (2, 2, 1)],
        )

    def test_opcodes(self) -> None:
        def chk_ops(
            a: Sequence[Any], b: Sequence[Any], expected_codes: List[Any]
        ) -> None:
            s = self._PatienceSequenceMatcher(None, a, b)
            self.assertEqual(expected_codes, s.get_opcodes())

        chk_ops("", "", [])
        chk_ops([], [], [])
        chk_ops("abc", "", [("delete", 0, 3, 0, 0)])
        chk_ops("", "abc", [("insert", 0, 0, 0, 3)])
        chk_ops("abcd", "abcd", [("equal", 0, 4, 0, 4)])
        chk_ops(
            "abcd", "abce", [("equal", 0, 3, 0, 3), ("replace", 3, 4, 3, 4)]
        )
        chk_ops(
            "eabc",
            "abce",
            [
                ("delete", 0, 1, 0, 0),
                ("equal", 1, 4, 0, 3),
                ("insert", 4, 4, 3, 4),
            ],
        )
        chk_ops(
            "eabce", "abce", [("delete", 0, 1, 0, 0), ("equal", 1, 5, 0, 4)]
        )
        chk_ops(
            "abcde",
            "abXde",
            [
                ("equal", 0, 2, 0, 2),
                ("replace", 2, 3, 2, 3),
                ("equal", 3, 5, 3, 5),
            ],
        )
        chk_ops(
            "abcde",
            "abXYZde",
            [
                ("equal", 0, 2, 0, 2),
                ("replace", 2, 3, 2, 5),
                ("equal", 3, 5, 5, 7),
            ],
        )
        chk_ops(
            "abde",
            "abXYZde",
            [
                ("equal", 0, 2, 0, 2),
                ("insert", 2, 2, 2, 5),
                ("equal", 2, 4, 5, 7),
            ],
        )
        chk_ops(
            "abcdefghijklmnop",
            "abcdefxydefghijklmnop",
            [
                ("equal", 0, 6, 0, 6),
                ("insert", 6, 6, 6, 11),
                ("equal", 6, 16, 11, 21),
            ],
        )
        chk_ops(
            ["hello there\n", "world\n", "how are you today?\n"],
            ["hello there\n", "how are you today?\n"],
            [
                ("equal", 0, 1, 0, 1),
                ("delete", 1, 2, 1, 1),
                ("equal", 2, 3, 1, 2),
            ],
        )
        chk_ops(
            "aBccDe",
            "abccde",
            [
                ("equal", 0, 1, 0, 1),
                ("replace", 1, 5, 1, 5),
                ("equal", 5, 6, 5, 6),
            ],
        )
        chk_ops(
            "aBcDec",
            "abcdec",
            [
                ("equal", 0, 1, 0, 1),
                ("replace", 1, 2, 1, 2),
                ("equal", 2, 3, 2, 3),
                ("replace", 3, 4, 3, 4),
                ("equal", 4, 6, 4, 6),
            ],
        )
        chk_ops(
            "aBcdEcdFg",
            "abcdecdfg",
            [
                ("equal", 0, 1, 0, 1),
                ("replace", 1, 8, 1, 8),
                ("equal", 8, 9, 8, 9),
            ],
        )
        chk_ops(
            "aBcdEeXcdFg",
            "abcdecdfg",
            [
                ("equal", 0, 1, 0, 1),
                ("replace", 1, 2, 1, 2),
                ("equal", 2, 4, 2, 4),
                ("delete", 4, 5, 4, 4),
                ("equal", 5, 6, 4, 5),
                ("delete", 6, 7, 5, 5),
                ("equal", 7, 9, 5, 7),
                ("replace", 9, 10, 7, 8),
                ("equal", 10, 11, 8, 9),
            ],
        )

    def test_grouped_opcodes(self) -> None:
        def chk_ops(
            a: Sequence[Any],
            b: Sequence[Any],
            expected_codes: List[Any],
            n: int = 3,
        ) -> None:
            s = self._PatienceSequenceMatcher(None, a, b)
            self.assertEqual(expected_codes, list(s.get_grouped_opcodes(n)))

        chk_ops("", "", [])
        chk_ops([], [], [])
        chk_ops("abc", "", [[("delete", 0, 3, 0, 0)]])
        chk_ops("", "abc", [[("insert", 0, 0, 0, 3)]])
        chk_ops("abcd", "abcd", [])
        chk_ops(
            "abcd", "abce", [[("equal", 0, 3, 0, 3), ("replace", 3, 4, 3, 4)]]
        )
        chk_ops(
            "eabc",
            "abce",
            [
                [
                    ("delete", 0, 1, 0, 0),
                    ("equal", 1, 4, 0, 3),
                    ("insert", 4, 4, 3, 4),
                ]
            ],
        )
        chk_ops(
            "abcdefghijklmnop",
            "abcdefxydefghijklmnop",
            [
                [
                    ("equal", 3, 6, 3, 6),
                    ("insert", 6, 6, 6, 11),
                    ("equal", 6, 9, 11, 14),
                ]
            ],
        )
        chk_ops(
            "abcdefghijklmnop",
            "abcdefxydefghijklmnop",
            [
                [
                    ("equal", 2, 6, 2, 6),
                    ("insert", 6, 6, 6, 11),
                    ("equal", 6, 10, 11, 15),
                ]
            ],
            4,
        )
        chk_ops(
            "Xabcdef",
            "abcdef",
            [[("delete", 0, 1, 0, 0), ("equal", 1, 4, 0, 3)]],
        )
        chk_ops(
            "abcdef",
            "abcdefX",
            [[("equal", 3, 6, 3, 6), ("insert", 6, 6, 6, 7)]],
        )

    def test_multiple_ranges(self) -> None:
        # There was an earlier bug where we used a bad set of ranges,
        # this triggers that specific bug, to make sure it doesn't regress
        self.assertDiffBlocks(
            "abcdefghijklmnop",
            "abcXghiYZQRSTUVWXYZijklmnop",
            [(0, 0, 3), (6, 4, 3), (9, 20, 7)],
        )

        self.assertDiffBlocks(
            "ABCd efghIjk  L",
            "AxyzBCn mo pqrstuvwI1 2  L",
            [(0, 0, 1), (1, 4, 2), (9, 19, 1), (12, 23, 3)],
        )

        # These are rot13 code snippets.
        self.assertDiffBlocks(
            '''\
    trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
    """
    gnxrf_netf = ['svyr*']
    gnxrf_bcgvbaf = ['ab-erphefr']

    qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr):
        sebz omeyvo.nqq vzcbeg fzneg_nqq, nqq_ercbegre_cevag, nqq_ercbegre_ahyy
        vs vf_dhvrg():
            ercbegre = nqq_ercbegre_ahyy
        ryfr:
            ercbegre = nqq_ercbegre_cevag
        fzneg_nqq(svyr_yvfg, abg ab_erphefr, ercbegre)


pynff pzq_zxqve(Pbzznaq):
'''.splitlines(True),
            '''\
    trg nqqrq jura lbh nqq n svyr va gur qverpgbel.

    --qel-eha jvyy fubj juvpu svyrf jbhyq or nqqrq, ohg abg npghnyyl
    nqq gurz.
    """
    gnxrf_netf = ['svyr*']
    gnxrf_bcgvbaf = ['ab-erphefr', 'qel-eha']

    qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr, qel_eha=Snyfr):
        vzcbeg omeyvo.nqq

        vs qel_eha:
            vs vf_dhvrg():
                # Guvf vf cbvagyrff, ohg V'q engure abg envfr na reebe
                npgvba = omeyvo.nqq.nqq_npgvba_ahyy
            ryfr:
  npgvba = omeyvo.nqq.nqq_npgvba_cevag
        ryvs vf_dhvrg():
            npgvba = omeyvo.nqq.nqq_npgvba_nqq
        ryfr:
       npgvba = omeyvo.nqq.nqq_npgvba_nqq_naq_cevag

        omeyvo.nqq.fzneg_nqq(svyr_yvfg, abg ab_erphefr, npgvba)


pynff pzq_zxqve(Pbzznaq):
'''.splitlines(True),
            [(0, 0, 1), (1, 4, 2), (9, 19, 1), (12, 23, 3)],
        )

    def test_patience_unified_diff(self) -> None:
        txt_a = ["hello there\n", "world\n", "how are you today?\n"]
        txt_b = ["hello there\n", "how are you today?\n"]
        unified_diff = patiencediff.unified_diff
        psm = self._PatienceSequenceMatcher
        self.assertEqual(
            [
                "--- \n",
                "+++ \n",
                "@@ -1,3 +1,2 @@\n",
                " hello there\n",
                "-world\n",
                " how are you today?\n",
            ],
            list(unified_diff(txt_a, txt_b, sequencematcher=psm)),
        )
        txt_a = [x + "\n" for x in "abcdefghijklmnop"]
        txt_b = [x + "\n" for x in "abcdefxydefghijklmnop"]
        # This is the result with LongestCommonSubstring matching
        self.assertEqual(
            [
                "--- \n",
                "+++ \n",
                "@@ -1,6 +1,11 @@\n",
                " a\n",
                " b\n",
                " c\n",
                "+d\n",
                "+e\n",
                "+f\n",
                "+x\n",
                "+y\n",
                " d\n",
                " e\n",
                " f\n",
            ],
            list(unified_diff(txt_a, txt_b)),
        )
        # And the patience diff
        self.assertEqual(
            [
                "--- \n",
                "+++ \n",
                "@@ -4,6 +4,11 @@\n",
                " d\n",
                " e\n",
                " f\n",
                "+x\n",
                "+y\n",
                "+d\n",
                "+e\n",
                "+f\n",
                " g\n",
                " h\n",
                " i\n",
            ],
            list(unified_diff(txt_a, txt_b, sequencematcher=psm)),
        )

    def test_patience_unified_diff_with_dates(self) -> None:
        txt_a = ["hello there\n", "world\n", "how are you today?\n"]
        txt_b = ["hello there\n", "how are you today?\n"]
        unified_diff = patiencediff.unified_diff
        psm = self._PatienceSequenceMatcher
        self.assertEqual(
            [
                "--- a\t2008-08-08\n",
                "+++ b\t2008-09-09\n",
                "@@ -1,3 +1,2 @@\n",
                " hello there\n",
                "-world\n",
                " how are you today?\n",
            ],
            list(
                unified_diff(
                    txt_a,
                    txt_b,
                    fromfile="a",
                    tofile="b",
                    fromfiledate="2008-08-08",
                    tofiledate="2008-09-09",
                    sequencematcher=psm,
                )
            ),
        )


class TestPatienceDiffLibFiles(unittest.TestCase):
    def setUp(self) -> None:
        super().setUp()
        self._PatienceSequenceMatcher: Type[difflib.SequenceMatcher] = (
            _patiencediff_py.PatienceSequenceMatcher_py
        )
        self.test_dir = tempfile.mkdtemp()
        self.addCleanup(lambda: shutil.rmtree(self.test_dir))

    def test_patience_unified_diff_files(self) -> None:
        txt_a = ["hello there\n", "world\n", "how are you today?\n"]
        txt_b = ["hello there\n", "how are you today?\n"]
        with open(os.path.join(self.test_dir, "a1"), "w") as f:
            f.writelines(txt_a)
        with open(os.path.join(self.test_dir, "b1"), "w") as f:
            f.writelines(txt_b)

        unified_diff_files = patiencediff.unified_diff_files
        psm = self._PatienceSequenceMatcher

        old_pwd = os.getcwd()
        os.chdir(self.test_dir)
        try:
            self.assertEqual(
                [
                    "--- a1\n",
                    "+++ b1\n",
                    "@@ -1,3 +1,2 @@\n",
                    " hello there\n",
                    "-world\n",
                    " how are you today?\n",
                ],
                list(unified_diff_files("a1", "b1", sequencematcher=psm)),
            )
        finally:
            os.chdir(old_pwd)

        txt_a = [x + "\n" for x in "abcdefghijklmnop"]
        txt_b = [x + "\n" for x in "abcdefxydefghijklmnop"]
        with open(os.path.join(self.test_dir, "a2"), "w") as f:
            f.writelines(txt_a)
        with open(os.path.join(self.test_dir, "b2"), "w") as f:
            f.writelines(txt_b)

        # This is the result with LongestCommonSubstring matching
        os.chdir(self.test_dir)
        try:
            self.assertEqual(
                [
                    "--- a2\n",
                    "+++ b2\n",
                    "@@ -1,6 +1,11 @@\n",
                    " a\n",
                    " b\n",
                    " c\n",
                    "+d\n",
                    "+e\n",
                    "+f\n",
                    "+x\n",
                    "+y\n",
                    " d\n",
                    " e\n",
                    " f\n",
                ],
                list(unified_diff_files("a2", "b2")),
            )

            # And the patience diff
            self.assertEqual(
                [
                    "--- a2\n",
                    "+++ b2\n",
                    "@@ -4,6 +4,11 @@\n",
                    " d\n",
                    " e\n",
                    " f\n",
                    "+x\n",
                    "+y\n",
                    "+d\n",
                    "+e\n",
                    "+f\n",
                    " g\n",
                    " h\n",
                    " i\n",
                ],
                list(unified_diff_files("a2", "b2", sequencematcher=psm)),
            )
        finally:
            os.chdir(old_pwd)


class TestPatienceDiffLib_rs(TestPatienceDiffLib):
    """Test class for the Rust implementation using PyO3 bindings."""

    def setUp(self) -> None:
        super(TestPatienceDiffLib, self).setUp()
        try:
            from . import _patiencediff_rs
        except ImportError:
            self.skipTest("Rust extension not built")
        self._unique_lcs = _patiencediff_rs.unique_lcs_rs
        self._recurse_matches = _patiencediff_rs.recurse_matches_rs
        self._PatienceSequenceMatcher = (
            _patiencediff_rs.PatienceSequenceMatcher_rs
        )

    def test_unhashable(self) -> None:
        """We should get a proper exception here."""
        # We need to be able to hash items in the sequence, lists are
        # unhashable, and thus cannot be diffed
        self.assertRaises(
            TypeError, self._PatienceSequenceMatcher, None, [[]], []
        )
        self.assertRaises(
            TypeError, self._PatienceSequenceMatcher, None, ["valid", []], []
        )
        self.assertRaises(
            TypeError, self._PatienceSequenceMatcher, None, ["valid"], [[]]
        )
        self.assertRaises(
            TypeError,
            self._PatienceSequenceMatcher,
            None,
            ["valid"],
            ["valid", []],
        )


class TestPatienceDiffLibFiles_rs(TestPatienceDiffLibFiles):
    """Test class for file operations with the Rust implementation."""

    def setUp(self) -> None:
        super().setUp()
        try:
            from . import _patiencediff_rs
        except ImportError:
            self.skipTest("Rust extension not built")
        self._PatienceSequenceMatcher = (
            _patiencediff_rs.PatienceSequenceMatcher_rs
        )


class TestUsingCompiledIfAvailable(unittest.TestCase):
    def test_PatienceSequenceMatcher(self) -> None:
        try:
            from ._patiencediff_rs import PatienceSequenceMatcher_rs

            self.assertIs(
                PatienceSequenceMatcher_rs,
                patiencediff.PatienceSequenceMatcher,
            )
        except ImportError:
            from ._patiencediff_py import PatienceSequenceMatcher_py

            self.assertIs(
                PatienceSequenceMatcher_py,
                patiencediff.PatienceSequenceMatcher,
            )

    def test_unique_lcs(self) -> None:
        try:
            from ._patiencediff_rs import unique_lcs_rs

            self.assertIs(unique_lcs_rs, patiencediff.unique_lcs)
        except ImportError:
            from ._patiencediff_py import unique_lcs_py

            self.assertIs(unique_lcs_py, patiencediff.unique_lcs)

    def test_recurse_matches(self) -> None:
        try:
            from ._patiencediff_rs import recurse_matches_rs

            self.assertIs(recurse_matches_rs, patiencediff.recurse_matches)
        except ImportError:
            from ._patiencediff_py import recurse_matches_py

            self.assertIs(recurse_matches_py, patiencediff.recurse_matches)

    def test_run_implementation(self) -> None:
        """Test that we can run the implementation that was loaded."""
        # Simple test with some basic strings
        a = "abcde"
        b = "abXde"

        # Create a matcher and get blocks
        matcher = patiencediff.PatienceSequenceMatcher(None, a, b)
        blocks = matcher.get_matching_blocks()

        # Validate results - we should get two blocks plus sentinel
        self.assertEqual(3, len(blocks))
        self.assertEqual((0, 0, 2), blocks[0])  # "ab" match
        self.assertEqual((3, 3, 2), blocks[1])  # "de" match
        self.assertEqual((5, 5, 0), blocks[2])  # sentinel

        # Test that unique_lcs works
        matches = patiencediff.unique_lcs(a, b)
        self.assertEqual([(0, 0), (1, 1), (3, 3), (4, 4)], matches)


if __name__ == "__main__":
    # Check which implementation is loaded
    import importlib.util

    if importlib.util.find_spec("patiencediff._patiencediff_rs") is not None:
        print("Rust extension is loaded successfully!")
    else:
        print("Rust extension is not available, using Python implementation")

    # Run the tests
    unittest.main()
