import unittest
import sys
import types
from datetime import datetime, timedelta, timezone

stub = types.ModuleType('pydantic_settings')


class BaseSettings:
    def __init__(self, **kwargs):
        for name, value in self.__class__.__dict__.items():
            if name.isupper():
                setattr(self, name, kwargs.get(name, value))


stub.BaseSettings = BaseSettings
sys.modules.setdefault('pydantic_settings', stub)

from algorithms.gear_parent_episode import (
    GroupEpisodeInput,
    EpisodeState,
    build_episode_plan,
    compute_prior_bonus_components,
    continuity_score,
)


class GearParentEpisodeTest(unittest.TestCase):
    def test_continuity_score_prefers_member_overlap_and_near_center(self):
        current = GroupEpisodeInput(
            group_key='ZHEDAIYU02394',
            normalized_parent_name='ZHEDAIYU02394',
            sub_cluster_id=1,
            member_mmsis=['100', '200', '300'],
            member_count=3,
            center_lat=35.0,
            center_lon=129.0,
        )
        previous = EpisodeState(
            episode_id='ep-prev',
            lineage_key='ZHEDAIYU02394',
            group_key='ZHEDAIYU02394',
            normalized_parent_name='ZHEDAIYU02394',
            current_sub_cluster_id=0,
            member_mmsis=['100', '200', '400'],
            member_count=3,
            center_lat=35.02,
            center_lon=129.01,
            last_snapshot_time=datetime.now(timezone.utc),
            status='ACTIVE',
        )
        score, overlap_count, distance_nm = continuity_score(current, previous)
        self.assertGreaterEqual(overlap_count, 2)
        self.assertGreater(score, 0.45)
        self.assertLess(distance_nm, 12.0)

    def test_build_episode_plan_creates_merge_episode(self):
        now = datetime.now(timezone.utc)
        current = GroupEpisodeInput(
            group_key='JINSHI',
            normalized_parent_name='JINSHI',
            sub_cluster_id=0,
            member_mmsis=['a', 'b', 'c', 'd'],
            member_count=4,
            center_lat=35.0,
            center_lon=129.0,
        )
        previous_a = EpisodeState(
            episode_id='ep-a',
            lineage_key='JINSHI',
            group_key='JINSHI',
            normalized_parent_name='JINSHI',
            current_sub_cluster_id=1,
            member_mmsis=['a', 'b'],
            member_count=2,
            center_lat=35.0,
            center_lon=129.0,
            last_snapshot_time=now - timedelta(minutes=5),
            status='ACTIVE',
        )
        previous_b = EpisodeState(
            episode_id='ep-b',
            lineage_key='JINSHI',
            group_key='JINSHI',
            normalized_parent_name='JINSHI',
            current_sub_cluster_id=2,
            member_mmsis=['c', 'd'],
            member_count=2,
            center_lat=35.01,
            center_lon=129.01,
            last_snapshot_time=now - timedelta(minutes=5),
            status='ACTIVE',
        )
        plan = build_episode_plan([current], {'JINSHI': [previous_a, previous_b]})
        assignment = plan.assignments[current.key]
        self.assertEqual(assignment.continuity_source, 'MERGE_NEW')
        self.assertEqual(set(assignment.merged_from_episode_ids), {'ep-a', 'ep-b'})
        self.assertEqual(plan.merged_episode_targets['ep-a'], assignment.episode_id)
        self.assertEqual(plan.merged_episode_targets['ep-b'], assignment.episode_id)

    def test_build_episode_plan_marks_split_continue_and_split_new(self):
        now = datetime.now(timezone.utc)
        previous = EpisodeState(
            episode_id='ep-prev',
            lineage_key='A01859',
            group_key='A01859',
            normalized_parent_name='A01859',
            current_sub_cluster_id=0,
            member_mmsis=['a', 'b', 'c', 'd'],
            member_count=4,
            center_lat=35.0,
            center_lon=129.0,
            last_snapshot_time=now - timedelta(minutes=5),
            status='ACTIVE',
        )
        current_a = GroupEpisodeInput(
            group_key='A01859',
            normalized_parent_name='A01859',
            sub_cluster_id=1,
            member_mmsis=['a', 'b', 'c'],
            member_count=3,
            center_lat=35.0,
            center_lon=129.0,
        )
        current_b = GroupEpisodeInput(
            group_key='A01859',
            normalized_parent_name='A01859',
            sub_cluster_id=2,
            member_mmsis=['c', 'd'],
            member_count=2,
            center_lat=35.02,
            center_lon=129.02,
        )
        plan = build_episode_plan([current_a, current_b], {'A01859': [previous]})
        sources = {plan.assignments[current_a.key].continuity_source, plan.assignments[current_b.key].continuity_source}
        self.assertIn('SPLIT_CONTINUE', sources)
        self.assertIn('SPLIT_NEW', sources)

    def test_compute_prior_bonus_components_caps_total_bonus(self):
        observed_at = datetime.now(timezone.utc)
        bonuses = compute_prior_bonus_components(
            observed_at=observed_at,
            normalized_parent_name='JINSHI',
            episode_id='ep-1',
            candidate_mmsi='412333326',
            episode_prior_stats={
                ('ep-1', '412333326'): {
                    'seen_count': 12,
                    'top1_count': 5,
                    'avg_score': 0.88,
                    'last_seen_at': observed_at - timedelta(hours=1),
                },
            },
            lineage_prior_stats={
                ('JINSHI', '412333326'): {
                    'seen_count': 24,
                    'top1_count': 6,
                    'top3_count': 10,
                    'avg_score': 0.82,
                    'last_seen_at': observed_at - timedelta(hours=3),
                },
            },
            label_prior_stats={
                ('JINSHI', '412333326'): {
                    'session_count': 4,
                    'last_labeled_at': observed_at - timedelta(days=1),
                },
            },
        )
        self.assertGreater(bonuses['episodePriorBonus'], 0.0)
        self.assertGreater(bonuses['lineagePriorBonus'], 0.0)
        self.assertGreater(bonuses['labelPriorBonus'], 0.0)
        self.assertLessEqual(bonuses['priorBonusTotal'], 0.20)


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