from __future__ import annotations

import sys
import unittest
from pathlib import Path


PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT / "src"))

from modular_recurrences import (  # noqa: E402
    FIBONACCI,
    LUCAS,
    PELL,
    SequenceSpec,
    direct_terms,
    orbit_statistics,
)


class OrbitTests(unittest.TestCase):
    def test_known_pisano_periods(self) -> None:
        expected = {2: 3, 3: 8, 4: 6, 5: 20, 7: 16, 8: 12, 9: 24, 10: 60}
        for modulus, period in expected.items():
            with self.subTest(modulus=modulus):
                self.assertEqual(orbit_statistics(FIBONACCI, modulus).period, period)

    def test_published_and_independent_counts(self) -> None:
        cases = [
            (FIBONACCI, 7, 7, 16),
            (FIBONACCI, 49, 37, 112),
            (FIBONACCI, 121, 67, 110),
            (FIBONACCI, 169, 117, 364),
            (LUCAS, 125, 44, 100),
            (LUCAS, 169, 156, 364),
            (PELL, 169, 13, 28),
            (PELL, 31**2, 22, 30),
        ]
        for spec, modulus, count, period in cases:
            with self.subTest(sequence=spec.name, modulus=modulus):
                stats = orbit_statistics(spec, modulus)
                self.assertEqual(stats.residue_count, count)
                self.assertEqual(stats.period, period)

    def test_pair_period_matches_direct_terms_for_small_moduli(self) -> None:
        for spec in (FIBONACCI, LUCAS, PELL):
            for modulus in range(2, 20):
                with self.subTest(sequence=spec.name, modulus=modulus):
                    stats = orbit_statistics(spec, modulus)
                    values = direct_terms(spec, modulus, stats.period)
                    self.assertEqual(len(set(values)), stats.residue_count)
                    self.assertEqual(
                        direct_terms(spec, modulus, stats.period + 2)[-2:],
                        [spec.x0 % modulus, spec.x1 % modulus],
                    )

    def test_noninvertible_recurrence_has_preperiod(self) -> None:
        altered = SequenceSpec(
            name="Noninvertible example",
            x0=0,
            x1=1,
            a=1,
            b=2,
            definition="x(n+2)=x(n+1)+2x(n)",
        )
        stats = orbit_statistics(altered, 4)
        self.assertEqual(stats.preperiod, 2)
        self.assertEqual(stats.period, 2)
        self.assertEqual(stats.residue_count, 3)
        self.assertFalse(stats.returned_to_start)

    def test_pell_exception_identity(self) -> None:
        values = direct_terms(PELL, 13**2, 8)
        self.assertEqual(values[7], 0)


if __name__ == "__main__":
    unittest.main()

