"""Offline registration/config transactions; no child, Redis, DB or Web connection."""
from copy import deepcopy
import json
from pathlib import Path
import threading
import time
from unittest.mock import MagicMock

import pytest

from app.event_artifact_contract import ArtifactConfig
from app.event_artifact_store import ArtifactStore
from app.pose_config import execution_plan
from app.secure_control import ControlService, Start
from app.stream_config_store import ConfigPersistenceError
from app.stream_manager import StreamManager
from control_sidecar.main import validate_event
from control_sidecar.protocol import BoundaryError
from test_secure_bridge import start_body

ROOT = Path(__file__).resolve().parents[1]


class StubGroup:
    def __init__(self, spec):
        self.spec = deepcopy(spec)
        self.stream_id = spec['stream_id']
        self.observed_state = 'stopped'
        self._pending_restart = None
        self._teardown_lock = threading.RLock()
        self.starts = self.restarts = 0

    def start(self):
        if self.observed_state != 'running':
            self.starts += 1
            self.observed_state = 'running'

    def check_restart(self, candidate):
        pass

    def restart(self, candidate):
        self.restarts += 1
        self.spec = deepcopy(candidate)


def service(tmp_path, monkeypatch):
    manager = StreamManager(str(tmp_path/'base.yaml'), str(tmp_path/'overlay.yaml'))
    monkeypatch.setattr(manager, '_new_group', StubGroup)
    template = json.loads((ROOT/'configs/control_templates/medium.json').read_text())
    return manager, ControlService(manager, 'AI-test', template)


def test_new_registration_saved_once_equivalent_roi_noop_and_changed_config(tmp_path, monkeypatch):
    manager, control = service(tmp_path, monkeypatch)
    d = start_body(9876)
    group = control.start_camera(Start(json.dumps(d)))
    persisted = Path(manager.overlay_path).read_bytes()
    assert len(manager.config_store.read()['added']) == 1
    assert group.spec['camera_id'] == 9876 and group.spec['source_id'] == '9876'
    assert not group.spec['autostart'] and group.starts == 1
    for form in ('missing', 'null', 'empty'):
        repeated = deepcopy(d)
        for s in repeated['scenarios']:
            if form == 'missing':
                s.pop('rois')
            elif form == 'null':
                s['rois'] = None
        assert control.start_camera(Start(json.dumps(repeated))) is group
        assert Path(manager.overlay_path).read_bytes() == persisted
    assert group.starts == 1 and group.restarts == 0
    d['rtspUrl'] = 'rtsp://camera.example.invalid/changed'
    control.start_camera(Start(json.dumps(d)))
    assert group.restarts == 1 and group.spec['uri'] == d['rtspUrl']
    assert len(manager.config_store.read()['added']) == 1


def test_new_registration_save_failure_does_not_register_or_start(tmp_path, monkeypatch):
    manager, control = service(tmp_path, monkeypatch)
    created = []
    def new_group(spec):
        group = StubGroup(spec)
        created.append(group)
        return group
    monkeypatch.setattr(manager, '_new_group', new_group)
    def reject(change):
        raise ConfigPersistenceError()
    monkeypatch.setattr(manager.config_store, 'update', reject)
    with pytest.raises(ConfigPersistenceError):
        control.start_camera(Start(json.dumps(start_body(9876))))
    assert not manager._groups and not Path(manager.overlay_path).exists()
    assert len(created) == 1 and created[0].starts == 0


@pytest.mark.parametrize('name,model', [('medium','RFDETRMedium'), ('pose','RFDETRKeypointPreview'), ('fire','RFDETRFireSmoke')])
def test_prepared_templates_are_identity_free_and_resolve(name, model):
    template = json.loads((ROOT/f'configs/control_templates/{name}.json').read_text())
    assert not {'stream_id','source_id','server_id','camera_id','uri','host','username','password','frame_time_anchor'} & template.keys()
    assert execution_plan(dict(template, server_id='AI-test', source_id='9876'))['model'] == model


@pytest.mark.parametrize('allowlist,expected', [('', 'CLAIMED'), (None, 'CLAIMED'), ('1,2', 'SKIPPED')])
def test_new_camera_delivery_policy_without_database(tmp_path, monkeypatch, allowlist, expected):
    for key, value in dict(CONTEXTRA_EVENT_ARTIFACT_ENABLED='1', DATABASE_URL='unused',
            CONTEXTRA_EVENT_ENDPOINT='http://sidecar.invalid/internal/events',
            CONTEXTRA_ARTIFACT_PUBLIC_BASE='https://ai.example.invalid/artifacts',
            CONTEXTRA_ARTIFACT_ROOT=str(tmp_path)).items():
        monkeypatch.setenv(key, value)
    if allowlist is None:
        monkeypatch.delenv('CONTEXTRA_ARTIFACT_DELIVERY_CAMERAS', raising=False)
    else:
        monkeypatch.setenv('CONTEXTRA_ARTIFACT_DELIVERY_CAMERAS', allowlist)
    store = ArtifactStore.__new__(ArtifactStore)
    store.config = ArtifactConfig.from_environment()
    store.transaction = MagicMock()
    cursor = store.transaction.return_value.__enter__.return_value
    cursor.fetchone.return_value = {'body': {'serverId': 'AI-test'}}
    row = dict(confirmed_at=time.time(), payload={'cameraId': 9876}, event_id='event', claim_token='claim')
    result = store.prepare_delivery(row, {'serverId': 'AI-test'})
    assert cursor.execute.call_args.args[1][1] == expected
    assert (result is not None) == (expected == 'CLAIMED')


def test_relay_still_rejects_another_server():
    with pytest.raises(BoundaryError):
        validate_event({'serverId': 'another-server', 'events': []}, 'AI-test')
