import os
import sys
from datetime import datetime
from io import BytesIO
from unittest.mock import Mock

import pytest
from flask import Flask
from openpyxl import load_workbook

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from mbetterclient.web_dashboard import routes as routes_module
from mbetterclient.web_dashboard.routes import api_bp, is_bet_detail_winning, is_settled_bet_detail_winning


class DummyDetail:
    def __init__(self, match_id, amount, win_amount, result, outcome='WIN1', bet_id='bet1'):
        self.match_id = match_id
        self.amount = amount
        self.win_amount = win_amount
        self.result = result
        self.outcome = outcome
        self.bet_id = bet_id


class DummyBet:
    def __init__(self, uuid, bet_datetime):
        self.uuid = uuid
        self.bet_datetime = bet_datetime


class DummyMatch:
    def __init__(self, match_id, match_number=1):
        self.id = match_id
        self.match_number = match_number
        self.fighter1_township = 'A'
        self.fighter2_township = 'B'
        self.venue_kampala_township = 'V'
        self.status = 'completed'
        self.winning_outcomes = ['WIN1']
        self.result = 'WIN1'


class QueryStub:
    def __init__(self, rows, joins=None):
        self.rows = rows
        self.joins = joins or {}

    def join(self, *args, **kwargs):
        for join_target in args:
            model_name = getattr(join_target, '__name__', None)
            if model_name and model_name not in self.joins:
                self.joins[model_name] = []
        return self

    def filter(self, *args, **kwargs):
        filtered = self.rows
        for predicate in args:
            filtered = [row for row in filtered if self._matches_predicate(row, predicate)]
        return QueryStub(filtered, joins=self.joins)

    def filter_by(self, **kwargs):
        if not kwargs:
            return self
        filtered = [
            row for row in self.rows
            if all(getattr(row, key, None) == value for key, value in kwargs.items())
        ]
        return QueryStub(filtered, joins=self.joins)

    def order_by(self, *args, **kwargs):
        return self

    def all(self):
        return self.rows

    def first(self):
        return self.rows[0] if self.rows else None

    def _matches_predicate(self, row, predicate):
        if not isinstance(predicate, tuple) or len(predicate) != 3:
            return True

        operator, field_name, expected = predicate
        actual = self._resolve_field_value(row, field_name)

        if operator == '>=':
            return actual is not None and actual >= expected
        if operator == '<=':
            return actual is not None and actual <= expected
        if operator == '==':
            return actual == expected
        if operator == '!=':
            return actual != expected
        return True

    def _resolve_field_value(self, row, field_name):
        direct_value = getattr(row, field_name, None)
        if direct_value is not None:
            return direct_value

        if field_name == 'bet_datetime':
            bets = self.joins.get('BetModel', [])
            bet_id = getattr(row, 'bet_id', None)
            if bet_id is not None:
                for bet in bets:
                    if getattr(bet, 'uuid', None) == bet_id:
                        return getattr(bet, 'bet_datetime', None)

        return None


class DummySession:
    def __init__(self, bet_details, bets, matches, extraction_stats=None):
        self.bet_details = list(bet_details)
        self.bets = list(bets)
        self.matches = list(matches)
        self.extraction_stats = list(extraction_stats or [])

    def query(self, model):
        model_name = getattr(model, '__name__', str(model))
        if model_name == 'BetDetailModel':
            return QueryStub(self.bet_details, joins={'BetModel': self.bets})
        if model_name == 'BetModel':
            return QueryStub(self.bets)
        if model_name == 'MatchModel':
            return QueryStub(self.matches)
        if model_name == 'ExtractionStatsModel':
            return QueryStub(self.extraction_stats)
        return QueryStub([])

    def close(self):
        return None


class DummyDbManager:
    def __init__(self, session):
        self._session = session

    def get_session(self):
        return self._session


class DummyLoginManager:
    def _load_user(self):
        return None

    @property
    def unauthorized(self):
        return lambda: ('Unauthorized', 401)


class DummyModelColumn:
    def __init__(self, name):
        self.name = name

    def __ge__(self, other):
        return ('>=', self.name, other)

    def __le__(self, other):
        return ('<=', self.name, other)

    def __eq__(self, other):
        return ('==', self.name, other)

    def __ne__(self, other):
        return ('!=', self.name, other)

    def desc(self):
        return self


def _dummy_model(name, **columns):
    attrs = {'__name__': name}
    attrs.update(columns)
    return type(name, (), attrs)


DummyBetModel = _dummy_model(
    'BetModel',
    uuid=DummyModelColumn('uuid'),
    bet_datetime=DummyModelColumn('bet_datetime'),
)
DummyBetDetailModel = _dummy_model(
    'BetDetailModel',
    bet_id=DummyModelColumn('bet_id'),
    match_id=DummyModelColumn('match_id'),
    result=DummyModelColumn('result'),
)
DummyMatchModel = _dummy_model('MatchModel', id=DummyModelColumn('id'))
DummyExtractionStatsModel = _dummy_model(
    'ExtractionStatsModel',
    match_datetime=DummyModelColumn('match_datetime'),
)


@pytest.fixture
def isolated_api_blueprint():
    original_db_manager = api_bp.db_manager
    original_auth_manager = api_bp.auth_manager
    try:
        yield api_bp
    finally:
        api_bp.db_manager = original_db_manager
        api_bp.auth_manager = original_auth_manager


def make_test_client(session, blueprint):
    app = Flask(__name__)
    app.config['TESTING'] = True
    app.secret_key = 'test-secret'
    app.login_manager = DummyLoginManager()
    app.register_blueprint(blueprint, url_prefix='/api')
    blueprint.db_manager = DummyDbManager(session)
    blueprint.auth_manager = None
    return app.test_client()


def install_route_test_doubles(monkeypatch):
    monkeypatch.setattr(routes_module, 'get_venue_timezone', lambda db_manager: None)
    monkeypatch.setattr(routes_module, 'venue_to_utc_datetime', lambda value, db_manager: value)
    monkeypatch.setattr(routes_module, 'login_required', lambda func: func)

    import sys as _sys
    models_module = type(_sys)('mbetterclient.database.models')
    models_module.BetModel = DummyBetModel
    models_module.BetDetailModel = DummyBetDetailModel
    models_module.MatchModel = DummyMatchModel
    models_module.ExtractionStatsModel = DummyExtractionStatsModel
    monkeypatch.setitem(_sys.modules, 'mbetterclient.database.models', models_module)


def build_sample_excel_bytes(monkeypatch, blueprint):
    install_route_test_doubles(monkeypatch)
    detail = DummyDetail(match_id=10, amount=100.0, win_amount=250.0, result='lost')
    cancelled_detail = DummyDetail(match_id=10, amount=70.0, win_amount=0.0, result='cancelled', bet_id='bet-cancelled')
    out_of_window_detail = DummyDetail(match_id=99, amount=40.0, win_amount=500.0, result='lost', bet_id='bet-outside')
    bet = DummyBet(uuid='bet1', bet_datetime=datetime(2026, 5, 1, 12, 0, 0))
    cancelled_bet = DummyBet(uuid='bet-cancelled', bet_datetime=datetime(2026, 5, 1, 13, 0, 0))
    out_of_window_bet = DummyBet(uuid='bet-outside', bet_datetime=datetime(2026, 5, 2, 12, 0, 0))
    match = DummyMatch(match_id=10)
    match.status = 'ingame'
    outside_match = DummyMatch(match_id=99, match_number=2)
    extraction_stat = type('ExtractionStat', (), {'match_datetime': datetime(2026, 5, 1, 15, 0, 0), 'total_redistributed': 999.0})()
    session = DummySession(
        [detail, cancelled_detail, out_of_window_detail],
        [bet, cancelled_bet, out_of_window_bet],
        [match, outside_match],
        [extraction_stat],
    )
    client = make_test_client(session, blueprint)
    response = client.get('/api/reports/download-excel?date=2026-05-01')
    assert response.status_code == 200
    return response.data


def test_match_report_payout_uses_loaded_match():
    detail = DummyDetail(match_id=10, amount=100.0, win_amount=250.0, result='lost')
    match = DummyMatch(match_id=10)
    match.status = 'ingame'
    session = Mock()

    assert is_bet_detail_winning(detail, match, session) is True


def test_settled_winning_check_prefers_explicit_lost_for_completed_match():
    detail = DummyDetail(match_id=10, amount=100.0, win_amount=250.0, result='lost')
    match = DummyMatch(match_id=10)
    match.status = 'ingame'
    match.status = 'done'
    session = Mock()

    assert is_bet_detail_winning(detail, match, session) is True
    assert is_settled_bet_detail_winning(detail, match, session) is False


def test_settled_winning_check_uses_win_amount_for_completed_match_display():
    detail = DummyDetail(match_id=10, amount=100.0, win_amount=250.0, result='win')
    match = DummyMatch(match_id=10)
    match.status = 'done'
    session = Mock()

    assert is_settled_bet_detail_winning(detail, match, session) is True
    assert float(detail.win_amount) == 250.0


def test_match_reports_route_counts_winning_payout_after_loading_match(monkeypatch, isolated_api_blueprint):
    install_route_test_doubles(monkeypatch)
    match = DummyMatch(match_id=10)
    match.status = 'ingame'
    detail = DummyDetail(match_id=10, amount=100.0, win_amount=250.0, result='lost')
    cancelled_detail = DummyDetail(match_id=10, amount=15.0, win_amount=0.0, result='cancelled', bet_id='bet-cancelled')
    out_of_window_detail = DummyDetail(match_id=99, amount=50.0, win_amount=500.0, result='lost', bet_id='bet-outside')
    bet = DummyBet(uuid='bet1', bet_datetime=datetime(2026, 5, 1, 12, 0, 0))
    cancelled_bet = DummyBet(uuid='bet-cancelled', bet_datetime=datetime(2026, 5, 1, 13, 0, 0))
    out_of_window_bet = DummyBet(uuid='bet-outside', bet_datetime=datetime(2026, 5, 2, 12, 0, 0))
    outside_match = DummyMatch(match_id=99, match_number=2)
    client = make_test_client(
        DummySession(
            [detail, cancelled_detail, out_of_window_detail],
            [bet, cancelled_bet, out_of_window_bet],
            [match, outside_match],
        ),
        isolated_api_blueprint,
    )

    response = client.get('/api/reports/match-reports?date=2026-05-01')

    assert response.status_code == 200
    payload = response.get_json()
    assert payload['total'] == 1
    assert payload['matches'][0]['payin'] == 100.0
    assert payload['matches'][0]['payout'] == 0.0
    assert payload['matches'][0]['net'] == 100.0


def test_excel_summary_uses_same_payout_basis_as_daily_summary(monkeypatch, isolated_api_blueprint):
    workbook = load_workbook(BytesIO(build_sample_excel_bytes(monkeypatch, isolated_api_blueprint)))
    sheet = workbook['Daily Summary']

    assert sheet['B4'].value == '100.00'
    assert sheet['B5'].value == '0.00'
    assert sheet['B6'].value == '100.00'
    assert sheet['B7'].value == '1'
    assert sheet['B8'].value == '1'


def test_excel_download_returns_xlsx_response(monkeypatch, isolated_api_blueprint):
    install_route_test_doubles(monkeypatch)
    detail = DummyDetail(match_id=10, amount=100.0, win_amount=250.0, result='lost')
    out_of_window_detail = DummyDetail(match_id=99, amount=40.0, win_amount=500.0, result='lost', bet_id='bet-outside')
    bet = DummyBet(uuid='bet1', bet_datetime=datetime(2026, 5, 1, 12, 0, 0))
    out_of_window_bet = DummyBet(uuid='bet-outside', bet_datetime=datetime(2026, 5, 2, 12, 0, 0))
    match = DummyMatch(match_id=10)
    outside_match = DummyMatch(match_id=99, match_number=2)
    client = make_test_client(
        DummySession([detail, out_of_window_detail], [bet, out_of_window_bet], [match, outside_match]),
        isolated_api_blueprint,
    )

    response = client.get('/api/reports/download-excel?date=2026-05-01')

    assert response.status_code == 200
    assert response.mimetype == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'


def test_match_details_route_uses_explicit_join_for_bet_lookup(monkeypatch, isolated_api_blueprint):
    install_route_test_doubles(monkeypatch)
    detail = DummyDetail(match_id=10, amount=100.0, win_amount=250.0, result='lost')
    bet = DummyBet(uuid='bet1', bet_datetime=datetime(2026, 5, 1, 12, 0, 0))
    match = DummyMatch(match_id=10)
    client = make_test_client(
        DummySession([detail], [bet], [match]),
        isolated_api_blueprint,
    )

    response = client.get('/api/reports/match-details?date=2026-05-01&match_id=10')

    assert response.status_code == 200
    payload = response.get_json()
    assert payload['total'] == 0
