"""Exercise job lifecycle against a fake API, without authenticated requests."""
import importlib.util
from pathlib import Path
import tempfile
import unittest

spec = importlib.util.spec_from_file_location('jobs', Path(__file__).with_name('ebay-bulk-search-csv-background-jobs.py'))
jobs = importlib.util.module_from_spec(spec)
spec.loader.exec_module(jobs)
ID='8cb25db5-a069-4b66-9279-2bf4d4704edb'
BODY={'keyword':'sony a6400 body','ebaySite':'ebay.com','count':60,'maxPages':3,'sold':True,'resultType':'download'}

class FakeApi:
    def __init__(self, states=('queued','running','complete')):
        self.states=iter(states);self.calls=[]
    def __call__(self, method, path, **kw):
        self.calls.append((method,path,kw))
        if method=='POST': return {'jobId':ID,'status':'queued','estimatedSeconds':None}
        if method=='DELETE': return {'jobId':ID,'status':'cancelled','items':[],'units':1}
        if kw.get('csv'): return b'itemId,title\r\n123456789012,"Camera"\r\n'
        return {'jobId':ID,'status':next(self.states),'items':[],'units':2,'totalItems':1,'expiresAt':'2026-10-25T01:00:00Z'}

class Tests(unittest.TestCase):
    def setUp(self):
        self.temp=tempfile.TemporaryDirectory();self.root=Path(self.temp.name);self.now=0
    def tearDown(self):self.temp.cleanup()
    def sleep(self,n):self.now+=n
    def run_fake(self,api,**kw):
        return jobs.run_job(api,BODY,self.root/'query.json',self.root/'query.csv',clock=lambda:self.now,sleep=self.sleep,**kw)
    def test_complete_lifecycle_downloads_only_terminal(self):
        api=FakeApi();self.run_fake(api)
        self.assertEqual([c[0] for c in api.calls],['POST','GET','GET','GET','GET'])
        self.assertIn('/download.csv',api.calls[-1][1]);self.assertTrue((self.root/'query.csv').exists())
    def test_failed_job_retains_manifest_no_csv(self):
        api=FakeApi(('failed',))
        with self.assertRaisesRegex(RuntimeError,'failed'):self.run_fake(api)
        self.assertTrue((self.root/'query.json').exists());self.assertFalse((self.root/'query.csv').exists())
    def test_timeout_cancels_and_keeps_partial_metadata(self):
        api=FakeApi(('running','running'))
        with self.assertRaises(TimeoutError):self.run_fake(api,wait_seconds=20)
        self.assertEqual(api.calls[-1][0],'DELETE')
        self.assertIn('cancelled',(self.root/'query.json').read_text())
    def test_resume_uses_saved_job_without_post(self):
        self.run_fake(FakeApi(('complete',)));api=FakeApi(('complete',));self.run_fake(api)
        self.assertFalse(any(c[0]=='POST' for c in api.calls))
    def test_changed_inputs_rejected(self):
        self.run_fake(FakeApi(('complete',)))
        with self.assertRaisesRegex(ValueError,'Inputs differ'):
            jobs.run_job(FakeApi(),{**BODY,'keyword':'different'},self.root/'query.json',self.root/'other.csv')
    def test_start_key_saved_before_post(self):
        def failure(*a,**kw):
            self.assertTrue((self.root/'query.json').exists())
            self.assertIn('idempotencyKey',(self.root/'query.json').read_text())
            raise RuntimeError('connection failed')
        with self.assertRaises(RuntimeError):self.run_fake(failure)
    def test_no_key_in_manifest(self):
        self.run_fake(FakeApi(('complete',)))
        self.assertNotIn('API-KEY',(self.root/'query.json').read_text())
    def test_poll_interval_enforced(self):
        with self.assertRaises(ValueError):self.run_fake(FakeApi(),poll_seconds=1)
    def test_cancelled_does_not_download(self):
        api=FakeApi(('cancelled',))
        with self.assertRaises(RuntimeError):self.run_fake(api)
        self.assertFalse(any(c[2].get('csv') for c in api.calls))
    def test_api_rejects_foreign_path_without_request(self):
        with self.assertRaises(ValueError):jobs.Api('fake')('GET','https://evil.invalid')

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