"""Tests for deterministic VIN year resolution. Covers: all three 30-year cycles (1980-2009, 2010-2039, 2040-2050), position 7 disambiguation, edge cases, and invalid inputs. """ import pytest from unittest.mock import patch from datetime import datetime from app.engines.gemini_engine import resolve_vin_year class TestSecondCycle: """Position 7 alphabetic -> 2010-2039 cycle (NHTSA MY2010+ requirement).""" def test_p_with_alpha_pos7_returns_2023(self): """P=2023 when position 7 is alphabetic (the bug that triggered this fix).""" # VIN: 1G1YE2D32P5602473 -- pos7='D' (alphabetic), pos10='P' assert resolve_vin_year("1G1YE2D32P5602473") == 2023 def test_a_with_alpha_pos7_returns_2010(self): """A=2010 when position 7 is alphabetic.""" assert resolve_vin_year("1G1YE2D12A5602473") == 2010 def test_l_with_alpha_pos7_returns_2020(self): """L=2020 when position 7 is alphabetic.""" assert resolve_vin_year("1G1YE2D12L5602473") == 2020 def test_9_with_alpha_pos7_returns_2039(self): """9=2039 when position 7 is alphabetic.""" assert resolve_vin_year("1G1YE2D1295602473") == 2039 def test_digit_1_with_alpha_pos7_returns_2031(self): """1=2031 when position 7 is alphabetic.""" assert resolve_vin_year("1G1YE2D1215602473") == 2031 def test_s_with_alpha_pos7_returns_2025(self): """S=2025 when position 7 is alphabetic.""" assert resolve_vin_year("1G1YE2D12S5602473") == 2025 def test_t_with_alpha_pos7_returns_2026(self): """T=2026 when position 7 is alphabetic.""" assert resolve_vin_year("1G1YE2D12T5602473") == 2026 class TestFirstCycle: """Position 7 numeric -> 1980-2009 cycle.""" def test_m_with_numeric_pos7_returns_1991(self): """M=1991 when position 7 is numeric.""" assert resolve_vin_year("1G1YE2132M5602473") == 1991 def test_n_with_numeric_pos7_returns_1992(self): """N=1992 when position 7 is numeric.""" assert resolve_vin_year("1G1YE2132N5602473") == 1992 def test_p_with_numeric_pos7_returns_1993(self): """P=1993 when position 7 is numeric.""" assert resolve_vin_year("1G1YE2132P5602473") == 1993 def test_y_with_numeric_pos7_returns_2000(self): """Y=2000 when position 7 is numeric.""" assert resolve_vin_year("1G1YE2132Y5602473") == 2000 class TestThirdCycle: """Position 7 numeric + third cycle year (2040-2050) is plausible.""" @patch("app.engines.gemini_engine.datetime") def test_a_with_numeric_pos7_returns_2040_when_plausible(self, mock_dt): """A=2040 when position 7 is numeric and year 2040 is plausible.""" mock_dt.now.return_value = datetime(2039, 1, 1) # 2039 + 2 = 2041 >= 2040, so third cycle is plausible assert resolve_vin_year("1G1YE2132A5602473") == 2040 @patch("app.engines.gemini_engine.datetime") def test_l_with_numeric_pos7_returns_2050_when_plausible(self, mock_dt): """L=2050 when position 7 is numeric and year 2050 is plausible.""" mock_dt.now.return_value = datetime(2049, 6, 1) assert resolve_vin_year("1G1YE2132L5602473") == 2050 @patch("app.engines.gemini_engine.datetime") def test_a_with_numeric_pos7_returns_1980_when_2040_not_plausible(self, mock_dt): """A=1980 when third cycle year (2040) exceeds max plausible.""" mock_dt.now.return_value = datetime(2026, 2, 20) # 2026 + 2 = 2028 < 2040, so third cycle not plausible -> first cycle assert resolve_vin_year("1G1YE2132A5602473") == 1980 @patch("app.engines.gemini_engine.datetime") def test_k_with_numeric_pos7_returns_2049_when_plausible(self, mock_dt): """K=2049 when position 7 is numeric and year is plausible.""" mock_dt.now.return_value = datetime(2048, 1, 1) assert resolve_vin_year("1G1YE2132K5602473") == 2049 class TestEdgeCases: """Invalid inputs and boundary conditions.""" def test_short_vin_returns_none(self): """VIN shorter than 17 chars returns None.""" assert resolve_vin_year("1G1YE2D32") is None def test_empty_vin_returns_none(self): """Empty string returns None.""" assert resolve_vin_year("") is None def test_invalid_year_code_returns_none(self): """Position 10 with invalid code (e.g., 'Z') returns None.""" # Z is not a valid year code assert resolve_vin_year("1G1YE2D32Z5602473") is None def test_lowercase_vin_handled(self): """Lowercase VIN characters are handled correctly.""" assert resolve_vin_year("1g1ye2d32p5602473") == 2023 def test_i_o_q_not_valid_year_codes(self): """Letters I, O, Q are not valid VIN year codes.""" # These are excluded from VINs entirely but test graceful handling assert resolve_vin_year("1G1YE2D32I5602473") is None assert resolve_vin_year("1G1YE2D32O5602473") is None assert resolve_vin_year("1G1YE2D32Q5602473") is None