import asyncio
import importlib.util
import sys
import types
from io import BytesIO
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from quart import Quart, g

class _NoOpLimiter:
  def limit(self, *args, **kwargs):
    return lambda fn: fn

async def _fake_token_required(required_scope=None):
  def decorator(func):
    async def wrapper(*args, **kwargs):
      return await func(user={'sub': 'user_123', 'scope': 'files'}, *args, **kwargs)
    wrapper.__name__ = func.__name__
    return wrapper
  return decorator

def _load_download_module():
  setup_stub = types.ModuleType('my_modules.app.setup')
  setup_stub.LIMITER = _NoOpLimiter()

  header_stub = types.ModuleType('my_modules.decoratory.header')

  def token_required(required_scope=None):
    def decorator(func):
      async def wrapper(*args, **kwargs):
        return await func(user={'sub': 'user_123', 'scope': 'files'}, *args, **kwargs)
      wrapper.__name__ = func.__name__
      return wrapper
    return decorator

  header_stub.token_required = token_required

  sys.modules['my_modules.app.setup'] = setup_stub
  sys.modules['my_modules.decoratory.header'] = header_stub

  module_path = Path(__file__).resolve().parents[1] / 'routes' / 'api' / 'download.py'
  spec = importlib.util.spec_from_file_location('api_download_under_test', module_path)
  module = importlib.util.module_from_spec(spec)
  spec.loader.exec_module(module)
  return module

class FakeConvex:
  def __init__(self, file_data=None):
    self.file_data = file_data
    self.info_calls = []
    self.storage_calls = []

  async def get_file_informations(self, file_id, user_id):
    self.info_calls.append((file_id, user_id))
    return self.file_data

  async def get_from_storage(self, storage_id):
    self.storage_calls.append(storage_id)
    return BytesIO(b'private bytes')

def _client(file_data):
  module = _load_download_module()
  app = Quart(__name__)
  app.secret_key = 'test-secret'
  app.convex = FakeConvex(file_data)
  app.register_blueprint(module.api_download_bp)

  @app.before_request
  async def wide_event_context():
    g.wide_event = {}

  return app, app.test_client()

def test_private_download_serves_owner_file():
  async def run_test():
    app, client = _client({
      'file_id': 'file_123',
      'file_name': 'report.txt',
      'content_type': 'text/plain',
      'db_image_url': 'storage_123',
      'uploaded_at': 1700000000000,
      'user_id': 'user_123',
    })

    response = await client.get('/api/files/file_123/download')

    assert response.status_code == 200
    assert await response.get_data() == b'private bytes'
    assert response.headers['Content-Type'].startswith('text/plain')
    assert 'report.txt' in response.headers['Content-Disposition']
    assert response.headers['Cache-Control'] == 'private, no-store'
    assert app.convex.info_calls == [('file_123', 'user_123')]
    assert app.convex.storage_calls == ['storage_123']

  asyncio.run(run_test())

def test_private_download_returns_404_when_file_is_not_owned():
  async def run_test():
    app, client = _client(None)

    response = await client.get('/api/files/missing/download')

    assert response.status_code == 404
    assert app.convex.info_calls == [('missing', 'user_123')]
    assert app.convex.storage_calls == []

  asyncio.run(run_test())

def test_private_download_returns_410_for_expired_file():
  async def run_test():
    app, client = _client({
      'file_id': 'file_123',
      'file_name': 'report.txt',
      'content_type': 'text/plain',
      'db_image_url': 'storage_123',
      'uploaded_at': 1700000000000,
      'expired': True,
    })

    response = await client.get('/api/files/file_123/download')

    assert response.status_code == 410
    assert app.convex.storage_calls == []

  asyncio.run(run_test())
