import logging
from copy import deepcopy
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Dict, List, Union

from pandas import DataFrame, to_datetime
from tabulate import tabulate

from freqtrade.constants import (DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT,
                                 Config)
from freqtrade.data.metrics import (calculate_cagr, calculate_csum, calculate_market_change,
                                    calculate_max_drawdown)
from freqtrade.misc import decimals_per_coin, file_dump_joblib, file_dump_json, round_coin_value
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename

logger = logging.getLogger(__name__)

def store_backtest_stats(
        recordfilename: Path, stats: Dict[str, DataFrame], dtappendix: str) -> None:
    """
    Stores backtest results
    :param recordfilename: Path object, which can either be a filename or a directory.
        Filenames will be appended with a timestamp right before the suffix
        while for directories, <directory>/backtest-result-<datetime>.json will be used as filename
    :param stats: Dataframe containing the backtesting statistics
    :param dtappendix: Datetime to use for the filename
    """
    if recordfilename.is_dir():
        filename = (recordfilename / f'backtest-result-{dtappendix}.json')
    else:
        filename = Path.joinpath(
            recordfilename.parent, f'{recordfilename.stem}-{dtappendix}'
        ).with_suffix(recordfilename.suffix)

    # Store metadata separately.
    file_dump_json(get_backtest_metadata_filename(filename), stats['metadata'])
    del stats['metadata']

    file_dump_json(filename, stats)

    latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN)
    file_dump_json(latest_filename, {'latest_backtest': str(filename.name)})

def store_backtest_signal_candles(
        recordfilename: Path, candles: Dict[str, Dict], dtappendix: str) -> Path:
    """
    Stores backtest trade signal candles
    :param recordfilename: Path object, which can either be a filename or a directory.
        Filenames will be appended with a timestamp right before the suffix
        while for directories, <directory>/backtest-result-<datetime>_signals.pkl will be used
        as filename
    :param stats: Dict containing the backtesting signal candles
    :param dtappendix: Datetime to use for the filename
    """
    if recordfilename.is_dir():
        filename = (recordfilename / f'backtest-result-{dtappendix}_signals.pkl')
    else:
        filename = Path.joinpath(
            recordfilename.parent, f'{recordfilename.stem}-{dtappendix}_signals.pkl'
        )

    file_dump_joblib(filename, candles)

    return filename

def _get_line_floatfmt(stake_currency: str) -> List[str]:
    """
    Generate floatformat (goes in line with _generate_result_line())
    """
    return ['s', 'd', '.2f', '.2f', f'.{decimals_per_coin(stake_currency)}f',
            '.2f', 'd', 's', 's']

def _get_line_header(first_column: str, stake_currency: str,
                     direction: str = 'Entries') -> List[str]:
    """
    Generate header lines (goes in line with _generate_result_line())
    """
    return [first_column, direction, 'Avg Profit %', 'Cum Profit %',
            f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
            'Win  Draw  Loss  Win%']

def generate_wins_draws_losses(wins, draws, losses):
    if wins > 0 and losses == 0:
        wl_ratio = '100'
    elif wins == 0:
        wl_ratio = '0'
    else:
        wl_ratio = f'{100.0 / (wins + draws + losses) * wins:.1f}' if losses > 0 else '100'
    return f'{wins:>4}  {draws:>4}  {losses:>4}  {wl_ratio:>4}'

def _generate_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict:
    """
    Generate one result dict, with "first_column" as key.
    """
    profit_sum = result['profit_ratio'].sum()
    # (end-capital - starting capital) / starting capital
    profit_total = result['profit_abs'].sum() / starting_balance

    return {
        'key': first_column,
        'trades': len(result),
        'profit_mean': result['profit_ratio'].mean() if len(result) > 0 else 0.0,
        'profit_mean_pct': result['profit_ratio'].mean() * 100.0 if len(result) > 0 else 0.0,
        'profit_sum': profit_sum,
        'profit_sum_pct': round(profit_sum * 100.0, 2),
        'profit_total_abs': result['profit_abs'].sum(),
        'profit_total': profit_total,
        'profit_total_pct': round(profit_total * 100.0, 2),
        'duration_avg': str(timedelta(
                            minutes=round(result['trade_duration'].mean()))
                            ) if not result.empty else '0:00',
        # 'duration_max': str(timedelta(
        #                     minutes=round(result['trade_duration'].max()))
        #                     ) if not result.empty else '0:00',
        # 'duration_min': str(timedelta(
        #                     minutes=round(result['trade_duration'].min()))
        #                     ) if not result.empty else '0:00',
        'wins': len(result[result['profit_abs'] > 0]),
        'draws': len(result[result['profit_abs'] == 0]),
        'losses': len(result[result['profit_abs'] < 0]),
    }

def generate_pair_metrics(pairlist: List[str], stake_currency: str, starting_balance: int,
                          results: DataFrame, skip_nan: bool = False) -> List[Dict]:
    """
    Generates and returns a list  for the given backtest data and the results dataframe
    :param pairlist: Pairlist used
    :param stake_currency: stake-currency - used to correctly name headers
    :param starting_balance: Starting balance
    :param results: Dataframe containing the backtest results
    :param skip_nan: Print "left open" open trades
    :return: List of Dicts containing the metrics per pair
    """

    tabular_data = []

    for pair in pairlist:
        result = results[results['pair'] == pair]
        if skip_nan and result['profit_abs'].isnull().all():
            continue

        tabular_data.append(_generate_result_line(result, starting_balance, pair))

    # Sort by total profit %:
    tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True)

    # Append Total
    tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL'))
    return tabular_data

def generate_tag_metrics(tag_type: str,
                         starting_balance: int,
                         results: DataFrame,
                         skip_nan: bool = False) -> List[Dict]:
    """
    Generates and returns a list of metrics for the given tag trades and the results dataframe
    :param starting_balance: Starting balance
    :param results: Dataframe containing the backtest results
    :param skip_nan: Print "left open" open trades
    :return: List of Dicts containing the metrics per pair
    """

    tabular_data = []

    if tag_type in results.columns:
        for tag, count in results[tag_type].value_counts().items():
            result = results[results[tag_type] == tag]
            if skip_nan and result['profit_abs'].isnull().all():
                continue

            tabular_data.append(_generate_result_line(result, starting_balance, tag))

        # Sort by total profit %:
        tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True)

        # Append Total
        tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL'))
        return tabular_data
    else:
        return []

def generate_exit_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
    """
    Generate small table outlining Backtest results
    :param max_open_trades: Max_open_trades parameter
    :param results: Dataframe containing the backtest result for one strategy
    :return: List of Dicts containing the metrics per Sell reason
    """
    tabular_data = []

    for reason, count in results['exit_reason'].value_counts().items():
        result = results.loc[results['exit_reason'] == reason]

        profit_mean = result['profit_ratio'].mean()
        profit_sum = result['profit_ratio'].sum()
        profit_total = profit_sum / max_open_trades

        tabular_data.append(
            {
                'exit_reason': reason,
                'trades': count,
                'wins': len(result[result['profit_abs'] > 0]),
                'draws': len(result[result['profit_abs'] == 0]),
                'losses': len(result[result['profit_abs'] < 0]),
                'profit_mean': profit_mean,
                'profit_mean_pct': round(profit_mean * 100, 2),
                'profit_sum': profit_sum,
                'profit_sum_pct': round(profit_sum * 100, 2),
                'profit_total_abs': result['profit_abs'].sum(),
                'profit_total': profit_total,
                'profit_total_pct': round(profit_total * 100, 2),
            }
        )
    return tabular_data

def generate_strategy_comparison(bt_stats: Dict) -> List[Dict]:
    """
    Generate summary per strategy
    :param bt_stats: Dict of <Strategyname: DataFrame> containing results for all strategies
    :return: List of Dicts containing the metrics per Strategy
    """

    tabular_data = []
    for strategy, result in bt_stats.items():
        tabular_data.append(deepcopy(result['results_per_pair'][-1]))
        # Update "key" to strategy (results_per_pair has it as "Total").
        tabular_data[-1]['key'] = strategy
        tabular_data[-1]['max_drawdown_account'] = result['max_drawdown_account']
        tabular_data[-1]['max_drawdown_abs'] = round_coin_value(
            result['max_drawdown_abs'], result['stake_currency'], False)
    return tabular_data

def generate_edge_table(results: dict) -> str:
    floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', 'd', 'd')
    tabular_data = []
    headers = ['Pair', 'Stoploss', 'Win Rate', 'Risk Reward Ratio',
               'Required Risk Reward', 'Expectancy', 'Total Number of Trades',
               'Average Duration (min)']

    for result in results.items():
        if result[1].nb_trades > 0:
            tabular_data.append([
                result[0],
                result[1].stoploss,
                result[1].winrate,
                result[1].risk_reward_ratio,
                result[1].required_risk_reward,
                result[1].expectancy,
                result[1].nb_trades,
                round(result[1].avg_trade_duration)
            ])

    # Ignore type as floatfmt does allow tuples but mypy does not know that
    return tabulate(tabular_data, headers=headers,
                    floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")

def _get_resample_from_period(period: str) -> str:
    if period == 'day':
        return '1d'
    if period == 'week':
        return '1w'
    if period == 'month':
        return '1M'
    raise ValueError(f"Period {period} is not supported.")

def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dict[str, Any]]:
    results = DataFrame.from_records(trade_list)
    if len(results) == 0:
        return []
    results['close_date'] = to_datetime(results['close_date'], utc=True)
    resample_period = _get_resample_from_period(period)
    resampled = results.resample(resample_period, on='close_date')
    stats = []
    for name, day in resampled:
        profit_abs = day['profit_abs'].sum().round(10)
        wins = sum(day['profit_abs'] > 0)
        draws = sum(day['profit_abs'] == 0)
        loses = sum(day['profit_abs'] < 0)
        stats.append(
            {
                'date': name.strftime('%d/%m/%Y'),
                'profit_abs': profit_abs,
                'wins': wins,
                'draws': draws,
                'loses': loses
            }
        )
    return stats

def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
    """ Generate overall trade statistics """
    if len(results) == 0:
        return {
            'wins': 0,
            'losses': 0,
            'draws': 0,
            'holding_avg': timedelta(),
            'winner_holding_avg': timedelta(),
            'loser_holding_avg': timedelta(),
        }

    winning_trades = results.loc[results['profit_ratio'] > 0]
    draw_trades = results.loc[results['profit_ratio'] == 0]
    losing_trades = results.loc[results['profit_ratio'] < 0]

    holding_avg = (timedelta(minutes=round(results['trade_duration'].mean()))
                   if not results.empty else timedelta())
    winner_holding_avg = (timedelta(minutes=round(winning_trades['trade_duration'].mean()))
                          if not winning_trades.empty else timedelta())
    loser_holding_avg = (timedelta(minutes=round(losing_trades['trade_duration'].mean()))
                         if not losing_trades.empty else timedelta())

    return {
        'wins': len(winning_trades),
        'losses': len(losing_trades),
        'draws': len(draw_trades),
        'holding_avg': holding_avg,
        'holding_avg_s': holding_avg.total_seconds(),
        'winner_holding_avg': winner_holding_avg,
        'winner_holding_avg_s': winner_holding_avg.total_seconds(),
        'loser_holding_avg': loser_holding_avg,
        'loser_holding_avg_s': loser_holding_avg.total_seconds(),
    }

def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
    """ Generate daily statistics """
    if len(results) == 0:
        return {
            'backtest_best_day': 0,
            'backtest_worst_day': 0,
            'backtest_best_day_abs': 0,
            'backtest_worst_day_abs': 0,
            'winning_days': 0,
            'draw_days': 0,
            'losing_days': 0,
            'daily_profit_list': [],
        }
    daily_profit_rel = results.resample('1d', on='close_date')['profit_ratio'].sum()
    daily_profit = results.resample('1d', on='close_date')['profit_abs'].sum().round(10)
    worst_rel = min(daily_profit_rel)
    best_rel = max(daily_profit_rel)
    worst = min(daily_profit)
    best = max(daily_profit)
    winning_days = sum(daily_profit > 0)
    draw_days = sum(daily_profit == 0)
    losing_days = sum(daily_profit < 0)
    daily_profit_list = [(str(idx.date()), val) for idx, val in daily_profit.items()]

    return {
        'backtest_best_day': best_rel,
        'backtest_worst_day': worst_rel,
        'backtest_best_day_abs': best,
        'backtest_worst_day_abs': worst,
        'winning_days': winning_days,
        'draw_days': draw_days,
        'losing_days': losing_days,
        'daily_profit': daily_profit_list,
    }

def generate_strategy_stats(pairlist: List[str],
                            strategy: str,
                            content: Dict[str, Any],
                            min_date: datetime, max_date: datetime,
                            market_change: float
                            ) -> Dict[str, Any]:
    """
    :param pairlist: List of pairs to backtest
    :param strategy: Strategy name
    :param content: Backtest result data in the format:
                    {'results: results, 'config: config}}.
    :param min_date: Backtest start date
    :param max_date: Backtest end date
    :param market_change: float indicating the market change
    :return: Dictionary containing results per strategy and a strategy summary.
    """
    results: Dict[str, DataFrame] = content['results']
    if not isinstance(results, DataFrame):
        return {}
    config = content['config']
    max_open_trades = min(config['max_open_trades'], len(pairlist))
    start_balance = config['dry_run_wallet']
    stake_currency = config['stake_currency']

    pair_results = generate_pair_metrics(pairlist, stake_currency=stake_currency,
                                         starting_balance=start_balance,
                                         results=results, skip_nan=False)

    enter_tag_results = generate_tag_metrics("enter_tag", starting_balance=start_balance,
                                             results=results, skip_nan=False)

    exit_reason_stats = generate_exit_reason_stats(max_open_trades=max_open_trades,
                                                   results=results)
    left_open_results = generate_pair_metrics(
        pairlist, stake_currency=stake_currency, starting_balance=start_balance,
        results=results.loc[results['exit_reason'] == 'force_exit'], skip_nan=True)

    daily_stats = generate_daily_stats(results)
    trade_stats = generate_trading_stats(results)
    best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'],
                    key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
    worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'],
                     key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
    winning_profit = results.loc[results['profit_abs'] > 0, 'profit_abs'].sum()
    losing_profit = results.loc[results['profit_abs'] < 0, 'profit_abs'].sum()
    profit_factor = winning_profit / abs(losing_profit) if losing_profit else 0.0

    backtest_days = (max_date - min_date).days or 1
    strat_stats = {
        'trades': results.to_dict(orient='records'),
        'locks': [lock.to_json() for lock in content['locks']],
        'best_pair': best_pair,
        'worst_pair': worst_pair,
        'results_per_pair': pair_results,
        'results_per_enter_tag': enter_tag_results,
        'exit_reason_summary': exit_reason_stats,
        'left_open_trades': left_open_results,
        # 'days_breakdown_stats': days_breakdown_stats,

        'total_trades': len(results),
        'trade_count_long': len(results.loc[~results['is_short']]),
        'trade_count_short': len(results.loc[results['is_short']]),
        'total_volume': float(results['stake_amount'].sum()),
        'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0,
        'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0,
        'profit_median': results['profit_ratio'].median() if len(results) > 0 else 0,
        'profit_total': results['profit_abs'].sum() / start_balance,
        'profit_total_long': results.loc[~results['is_short'], 'profit_abs'].sum() / start_balance,
        'profit_total_short': results.loc[results['is_short'], 'profit_abs'].sum() / start_balance,
        'profit_total_abs': results['profit_abs'].sum(),
        'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(),
        'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(),
        'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']),
        'profit_factor': profit_factor,
        'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
        'backtest_start_ts': int(min_date.timestamp() * 1000),
        'backtest_end': max_date.strftime(DATETIME_PRINT_FORMAT),
        'backtest_end_ts': int(max_date.timestamp() * 1000),
        'backtest_days': backtest_days,

        'backtest_run_start_ts': content['backtest_start_time'],
        'backtest_run_end_ts': content['backtest_end_time'],

        'trades_per_day': round(len(results) / backtest_days, 2),
        'market_change': market_change,
        'pairlist': pairlist,
        'stake_amount': config['stake_amount'],
        'stake_currency': config['stake_currency'],
        'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
        'starting_balance': start_balance,
        'dry_run_wallet': start_balance,
        'final_balance': content['final_balance'],
        'rejected_signals': content['rejected_signals'],
        'timedout_entry_orders': content['timedout_entry_orders'],
        'timedout_exit_orders': content['timedout_exit_orders'],
        'canceled_trade_entries': content['canceled_trade_entries'],
        'canceled_entry_orders': content['canceled_entry_orders'],
        'replaced_entry_orders': content['replaced_entry_orders'],
        'max_open_trades': max_open_trades,
        'max_open_trades_setting': (config['max_open_trades']
                                    if config['max_open_trades'] != float('inf') else -1),
        'timeframe': config['timeframe'],
        'timeframe_detail': config.get('timeframe_detail', ''),
        'timerange': config.get('timerange', ''),
        'enable_protections': config.get('enable_protections', False),
        'strategy_name': strategy,
        # Parameters relevant for backtesting
        'stoploss': config['stoploss'],
        'trailing_stop': config.get('trailing_stop', False),
        'trailing_stop_positive': config.get('trailing_stop_positive'),
        'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0),
        'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False),
        'use_custom_stoploss': config.get('use_custom_stoploss', False),
        'minimal_roi': config['minimal_roi'],
        'use_exit_signal': config['use_exit_signal'],
        'exit_profit_only': config['exit_profit_only'],
        'exit_profit_offset': config['exit_profit_offset'],
        'ignore_roi_if_entry_signal': config['ignore_roi_if_entry_signal'],
        **daily_stats,
        **trade_stats
    }

    try:
        max_drawdown_legacy, _, _, _, _, _ = calculate_max_drawdown(
            results, value_col='profit_ratio')
        (drawdown_abs, drawdown_start, drawdown_end, high_val, low_val,
         max_drawdown) = calculate_max_drawdown(
             results, value_col='profit_abs', starting_balance=start_balance)
        # max_relative_drawdown = Underwater
        (_, _, _, _, _, max_relative_drawdown) = calculate_max_drawdown(
             results, value_col='profit_abs', starting_balance=start_balance, relative=True)

        strat_stats.update({
            'max_drawdown': max_drawdown_legacy,  # Deprecated - do not use
            'max_drawdown_account': max_drawdown,
            'max_relative_drawdown': max_relative_drawdown,
            'max_drawdown_abs': drawdown_abs,
            'drawdown_start': drawdown_start.strftime(DATETIME_PRINT_FORMAT),
            'drawdown_start_ts': drawdown_start.timestamp() * 1000,
            'drawdown_end': drawdown_end.strftime(DATETIME_PRINT_FORMAT),
            'drawdown_end_ts': drawdown_end.timestamp() * 1000,

            'max_drawdown_low': low_val,
            'max_drawdown_high': high_val,
        })

        csum_min, csum_max = calculate_csum(results, start_balance)
        strat_stats.update({
            'csum_min': csum_min,
            'csum_max': csum_max
        })

    except ValueError:
        strat_stats.update({
            'max_drawdown': 0.0,
            'max_drawdown_account': 0.0,
            'max_relative_drawdown': 0.0,
            'max_drawdown_abs': 0.0,
            'max_drawdown_low': 0.0,
            'max_drawdown_high': 0.0,
            'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc),
            'drawdown_start_ts': 0,
            'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc),
            'drawdown_end_ts': 0,
            'csum_min': 0,
            'csum_max': 0
        })

    return strat_stats

def generate_backtest_stats(btdata: Dict[str, DataFrame],
                            all_results: Dict[str, Dict[str, Union[DataFrame, Dict]]],
                            min_date: datetime, max_date: datetime
                            ) -> Dict[str, Any]:
    """
    :param btdata: Backtest data
    :param all_results: backtest result - dictionary in the form:
                     { Strategy: {'results: results, 'config: config}}.
    :param min_date: Backtest start date
    :param max_date: Backtest end date
    :return: Dictionary containing results per strategy and a strategy summary.
    """
    result: Dict[str, Any] = {
        'metadata': {},
        'strategy': {},
        'strategy_comparison': [],
    }
    market_change = calculate_market_change(btdata, 'close')
    metadata = {}
    pairlist = list(btdata.keys())
    for strategy, content in all_results.items():
        strat_stats = generate_strategy_stats(pairlist, strategy, content,
                                              min_date, max_date, market_change=market_change)
        metadata[strategy] = {
            'run_id': content['run_id'],
            'backtest_start_time': content['backtest_start_time'],
        }
        result['strategy'][strategy] = strat_stats

    strategy_results = generate_strategy_comparison(bt_stats=result['strategy'])

    result['metadata'] = metadata
    result['strategy_comparison'] = strategy_results

    return result

###
# Start output section
###

def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: str) -> str:
    """
    Generates and returns a text table for the given backtest data and the results dataframe
    :param pair_results: List of Dictionaries - one entry per pair + final TOTAL row
    :param stake_currency: stake-currency - used to correctly name headers
    :return: pretty printed table with tabulate as string
    """

    headers = _get_line_header('Pair', stake_currency)
    floatfmt = _get_line_floatfmt(stake_currency)
    output = [[
        t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
        t['profit_total_pct'], t['duration_avg'],
        generate_wins_draws_losses(t['wins'], t['draws'], t['losses'])
    ] for t in pair_results]
    # Ignore type as floatfmt does allow tuples but mypy does not know that
    return tabulate(output, headers=headers,
                    floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")

def text_table_exit_reason(exit_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str:
    """
    Generate small table outlining Backtest results
    :param sell_reason_stats: Exit reason metrics
    :param stake_currency: Stakecurrency used
    :return: pretty printed table with tabulate as string
    """
    headers = [
        'Exit Reason',
        'Exits',
        'Win  Draws  Loss  Win%',
        'Avg Profit %',
        'Cum Profit %',
        f'Tot Profit {stake_currency}',
        'Tot Profit %',
    ]

    output = [[
        t.get('exit_reason', t.get('sell_reason')), t['trades'],
        generate_wins_draws_losses(t['wins'], t['draws'], t['losses']),
        t['profit_mean_pct'], t['profit_sum_pct'],
        round_coin_value(t['profit_total_abs'], stake_currency, False),
        t['profit_total_pct'],
    ] for t in exit_reason_stats]
    return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")

def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_currency: str) -> str:
    """
    Generates and returns a text table for the given backtest data and the results dataframe
    :param pair_results: List of Dictionaries - one entry per pair + final TOTAL row
    :param stake_currency: stake-currency - used to correctly name headers
    :return: pretty printed table with tabulate as string
    """
    if (tag_type == "enter_tag"):
        headers = _get_line_header("TAG", stake_currency)
    else:
        headers = _get_line_header("TAG", stake_currency, 'Exits')
    floatfmt = _get_line_floatfmt(stake_currency)
    output = [
        [
            t['key'] if t['key'] is not None and len(
                t['key']) > 0 else "OTHER",
            t['trades'],
            t['profit_mean_pct'],
            t['profit_sum_pct'],
            t['profit_total_abs'],
            t['profit_total_pct'],
            t['duration_avg'],
            generate_wins_draws_losses(
                t['wins'],
                t['draws'],
                t['losses'])] for t in tag_results]
    # Ignore type as floatfmt does allow tuples but mypy does not know that
    return tabulate(output, headers=headers,
                    floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")

def text_table_periodic_breakdown(days_breakdown_stats: List[Dict[str, Any]],
                                  stake_currency: str, period: str) -> str:
    """
    Generate small table with Backtest results by days
    :param days_breakdown_stats: Days breakdown metrics
    :param stake_currency: Stakecurrency used
    :return: pretty printed table with tabulate as string
    """
    headers = [
        period.capitalize(),
        f'Tot Profit {stake_currency}',
        'Wins',
        'Draws',
        'Losses',
    ]
    output = [[
        d['date'], round_coin_value(d['profit_abs'], stake_currency, False),
        d['wins'], d['draws'], d['loses'],
    ] for d in days_breakdown_stats]
    return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")

def text_table_strategy(strategy_results, stake_currency: str) -> str:
    """
    Generate summary table per strategy
    :param strategy_results: Dict of <Strategyname: DataFrame> containing results for all strategies
    :param stake_currency: stake-currency - used to correctly name headers
    :return: pretty printed table with tabulate as string
    """
    floatfmt = _get_line_floatfmt(stake_currency)
    headers = _get_line_header('Strategy', stake_currency)
    # _get_line_header() is also used for per-pair summary. Per-pair drawdown is mostly useless
    # therefore we slip this column in only for strategy summary here.
    headers.append('Drawdown')

    # Align drawdown string on the center two space separator.
    if 'max_drawdown_account' in strategy_results[0]:
        drawdown = [f'{t["max_drawdown_account"] * 100:.2f}' for t in strategy_results]
    else:
        # Support for prior backtest results
        drawdown = [f'{t["max_drawdown_per"]:.2f}' for t in strategy_results]

    dd_pad_abs = max([len(t['max_drawdown_abs']) for t in strategy_results])
    dd_pad_per = max([len(dd) for dd in drawdown])
    drawdown = [f'{t["max_drawdown_abs"]:>{dd_pad_abs}} {stake_currency}  {dd:>{dd_pad_per}}%'
                for t, dd in zip(strategy_results, drawdown)]

    output = [[
        t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
        t['profit_total_pct'], t['duration_avg'],
        generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), drawdown]
        for t, drawdown in zip(strategy_results, drawdown)]
    # Ignore type as floatfmt does allow tuples but mypy does not know that
    return tabulate(output, headers=headers,
                    floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")

def text_table_add_metrics(strat_results: Dict) -> str:
    if len(strat_results['trades']) > 0:
        best_trade = max(strat_results['trades'], key=lambda x: x['profit_ratio'])
        worst_trade = min(strat_results['trades'], key=lambda x: x['profit_ratio'])

        short_metrics = [
            ('', ''),  # Empty line to improve readability
            ('Long / Short',
             f"{strat_results.get('trade_count_long', 'total_trades')} / "
             f"{strat_results.get('trade_count_short', 0)}"),
            ('Total profit Long %', f"{strat_results['profit_total_long']:.2%}"),
            ('Total profit Short %', f"{strat_results['profit_total_short']:.2%}"),
            ('Absolute profit Long', round_coin_value(strat_results['profit_total_long_abs'],
                                                      strat_results['stake_currency'])),
            ('Absolute profit Short', round_coin_value(strat_results['profit_total_short_abs'],
                                                       strat_results['stake_currency'])),
        ] if strat_results.get('trade_count_short', 0) > 0 else []

        drawdown_metrics = []
        if 'max_relative_drawdown' in strat_results:
            # Compatibility to show old hyperopt results
            drawdown_metrics.append(
                ('Max % of account underwater', f"{strat_results['max_relative_drawdown']:.2%}")
            )
        drawdown_metrics.extend([
            ('Absolute Drawdown (Account)', f"{strat_results['max_drawdown_account']:.2%}")
            if 'max_drawdown_account' in strat_results else (
                'Drawdown', f"{strat_results['max_drawdown']:.2%}"),
            ('Absolute Drawdown', round_coin_value(strat_results['max_drawdown_abs'],
                                                   strat_results['stake_currency'])),
            ('Drawdown high', round_coin_value(strat_results['max_drawdown_high'],
                                               strat_results['stake_currency'])),
            ('Drawdown low', round_coin_value(strat_results['max_drawdown_low'],
                                              strat_results['stake_currency'])),
            ('Drawdown Start', strat_results['drawdown_start']),
            ('Drawdown End', strat_results['drawdown_end']),
        ])

        entry_adjustment_metrics = [
            ('Canceled Trade Entries', strat_results.get('canceled_trade_entries', 'N/A')),
            ('Canceled Entry Orders', strat_results.get('canceled_entry_orders', 'N/A')),
            ('Replaced Entry Orders', strat_results.get('replaced_entry_orders', 'N/A')),
        ] if strat_results.get('canceled_entry_orders', 0) > 0 else []

        # Newly added fields should be ignored if they are missing in strat_results. hyperopt-show
        # command stores these results and newer version of freqtrade must be able to handle old
        # results with missing new fields.
        metrics = [
            ('Backtesting from', strat_results['backtest_start']),
            ('Backtesting to', strat_results['backtest_end']),
            ('Max open trades', strat_results['max_open_trades']),
            ('', ''),  # Empty line to improve readability
            ('Total/Daily Avg Trades',
                f"{strat_results['total_trades']} / {strat_results['trades_per_day']}"),

            ('Starting balance', round_coin_value(strat_results['starting_balance'],
                                                  strat_results['stake_currency'])),
            ('Final balance', round_coin_value(strat_results['final_balance'],
                                               strat_results['stake_currency'])),
            ('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
                                                  strat_results['stake_currency'])),
            ('Total profit %', f"{strat_results['profit_total']:.2%}"),
            ('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'),
            ('Profit factor', f'{strat_results["profit_factor"]:.2f}' if 'profit_factor'
                              in strat_results else 'N/A'),
            ('Trades per day', strat_results['trades_per_day']),
            ('Avg. daily profit %',
             f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),
            ('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'],
                                                   strat_results['stake_currency'])),
            ('Total trade volume', round_coin_value(strat_results['total_volume'],
                                                    strat_results['stake_currency'])),
            *short_metrics,
            ('', ''),  # Empty line to improve readability
            ('Best Pair', f"{strat_results['best_pair']['key']} "
                          f"{strat_results['best_pair']['profit_sum']:.2%}"),
            ('Worst Pair', f"{strat_results['worst_pair']['key']} "
                           f"{strat_results['worst_pair']['profit_sum']:.2%}"),
            ('Best trade', f"{best_trade['pair']} {best_trade['profit_ratio']:.2%}"),
            ('Worst trade', f"{worst_trade['pair']} "
                            f"{worst_trade['profit_ratio']:.2%}"),

            ('Best day', round_coin_value(strat_results['backtest_best_day_abs'],
                                          strat_results['stake_currency'])),
            ('Worst day', round_coin_value(strat_results['backtest_worst_day_abs'],
                                           strat_results['stake_currency'])),
            ('Days win/draw/lose', f"{strat_results['winning_days']} / "
                f"{strat_results['draw_days']} / {strat_results['losing_days']}"),
            ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
            ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
            ('Rejected Entry signals', strat_results.get('rejected_signals', 'N/A')),
            ('Entry/Exit Timeouts',
             f"{strat_results.get('timedout_entry_orders', 'N/A')} / "
             f"{strat_results.get('timedout_exit_orders', 'N/A')}"),
            *entry_adjustment_metrics,
            ('', ''),  # Empty line to improve readability

            ('Min balance', round_coin_value(strat_results['csum_min'],
                                             strat_results['stake_currency'])),
            ('Max balance', round_coin_value(strat_results['csum_max'],
                                             strat_results['stake_currency'])),

            *drawdown_metrics,
            ('Market change', f"{strat_results['market_change']:.2%}"),
        ]

        return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl")
    else:
        start_balance = round_coin_value(strat_results['starting_balance'],
                                         strat_results['stake_currency'])
        stake_amount = round_coin_value(
            strat_results['stake_amount'], strat_results['stake_currency']
        ) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited'

        message = ("No trades made. "
                   f"Your starting balance was {start_balance}, "
                   f"and your stake was {stake_amount}."
                   )
        return message

def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: str,
                         backtest_breakdown=[]):
    """
    Print results for one strategy
    """
    # Print results
    print(f"Result for strategy {strategy}")
    table = text_table_bt_results(results['results_per_pair'], stake_currency=stake_currency)
    if isinstance(table, str):
        print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
    print(table)

    if (results.get('results_per_enter_tag') is not None
            or results.get('results_per_buy_tag') is not None):
        # results_per_buy_tag is deprecated and should be removed 2 versions after short golive.
        table = text_table_tags(
            "enter_tag",
            results.get('results_per_enter_tag', results.get('results_per_buy_tag')),
            stake_currency=stake_currency)

        if isinstance(table, str) and len(table) > 0:
            print(' ENTER TAG STATS '.center(len(table.splitlines()[0]), '='))
        print(table)

    exit_reasons = results.get('exit_reason_summary', results.get('sell_reason_summary'))
    table = text_table_exit_reason(exit_reason_stats=exit_reasons,
                                   stake_currency=stake_currency)
    if isinstance(table, str) and len(table) > 0:
        print(' EXIT REASON STATS '.center(len(table.splitlines()[0]), '='))
    print(table)

    table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency)
    if isinstance(table, str) and len(table) > 0:
        print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
    print(table)

    for period in backtest_breakdown:
        days_breakdown_stats = generate_periodic_breakdown_stats(
            trade_list=results['trades'], period=period)
        table = text_table_periodic_breakdown(days_breakdown_stats=days_breakdown_stats,
                                              stake_currency=stake_currency, period=period)
        if isinstance(table, str) and len(table) > 0:
            print(f' {period.upper()} BREAKDOWN '.center(len(table.splitlines()[0]), '='))
        print(table)

    table = text_table_add_metrics(results)
    if isinstance(table, str) and len(table) > 0:
        print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '='))
    print(table)

    if isinstance(table, str) and len(table) > 0:
        print('=' * len(table.splitlines()[0]))

    print()

def show_backtest_results(config: Config, backtest_stats: Dict):
    stake_currency = config['stake_currency']

    for strategy, results in backtest_stats['strategy'].items():
        show_backtest_result(
            strategy, results, stake_currency,
            config.get('backtest_breakdown', []))

    if len(backtest_stats['strategy']) > 1:
        # Print Strategy summary table

        table = text_table_strategy(backtest_stats['strategy_comparison'], stake_currency)
        print(f"{results['backtest_start']} -> {results['backtest_end']} |"
              f" Max open trades : {results['max_open_trades']}")
        print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
        print(table)
        print('=' * len(table.splitlines()[0]))
        print('\nFor more details, please look at the detail tables above')

def show_sorted_pairlist(config: Config, backtest_stats: Dict):
    if config.get('backtest_show_pair_list', False):
        for strategy, results in backtest_stats['strategy'].items():
            print(f"Pairs for Strategy {strategy}: \n[")
            for result in results['results_per_pair']:
                if result["key"] != 'TOTAL':
                    print(f'"{result["key"]}",  // {result["profit_mean"]:.2%}')
            print("]")


# Quick and dirty utility to get coordinates for transforming view into
# a bird's eye view. Useful in OCRs were the camera is in a fixed positioning
# viewing a straight plane.

import cv2
import numpy as np

def onTrackbarChange(trackbarValue):
    pass

def order_points(pts):
	# initialize a list of coordinates that will be ordered
	# such that the first entry in the list is the top-left,
	# the second entry is the top-right, the third is the
	# bottom-right, and the fourth is the bottom-left
	rect = np.zeros((4, 2), dtype = "float32")
	# the top-left point will have the smallest sum, whereas
	# the bottom-right point will have the largest sum
	s = pts.sum(axis = 1)
	rect[0] = pts[np.argmin(s)]
	rect[2] = pts[np.argmax(s)]
	# now, compute the difference between the points, the
	# top-right point will have the smallest difference,
	# whereas the bottom-left will have the largest difference
	diff = np.diff(pts, axis = 1)
	rect[1] = pts[np.argmin(diff)]
	rect[3] = pts[np.argmax(diff)]
	# return the ordered coordinates
	return rect

def expandPerspective(rect ,  width, height):
	'''Expand the perspective out to the image limits
	by finding intersection using point-slope form'''
	# Constants
	x = 0
	y = 1

	# Convert coordinate system
	rect[:,1] *= -1
	(tl, tr, br, bl) = rect

	# Find the slope of each of the 4 lines
	slopeTop = (tr[y]-tl[y]) / (tr[x]-tl[x])
	slopeBottom = (br[y]-bl[y]) / (br[x]-bl[x])
	slopeLeft = (tl[y]-bl[y]) / (tl[x]-bl[x])
	slopeRight = (tr[y]-br[y]) / (tr[x]-br[x])

	# Assign new points based on image size
	pointRight = width,0
	pointTop = 0,0
	pointBottom = width, height * -1.0
	pointLeft = 0, height* -1.0

	# Find where the new expanded lines intersect using point slope form
	def intersectoin (m1,m2,x1,x2,y1,y2,orig):

		x = ((m2*x2-m1*x1)-(y2-y1))/(m2-m1)
		#y = ((-1.0*m1*y2 + m1*m2*x2 + y1*m2 )-(m1*m2*x1))/(m2-m1)
		y = m1*(x - x1) + y1

		try:
		   x = round(x)
		   y = round(y)
		except:
			return orig
		return x, y

	new_tr =  intersectoin (slopeTop,slopeRight,pointTop[x],pointRight[x],pointTop[y],pointRight[y],tr)
	new_tl =  intersectoin (slopeTop,slopeLeft,pointTop[x],pointLeft[x],pointTop[y],pointLeft[y],tl)
	new_br =  intersectoin (slopeBottom,slopeRight,pointBottom[x],pointRight[x],pointBottom[y],pointRight[y],br)
	new_bl =  intersectoin (slopeBottom,slopeLeft,pointBottom[x],pointLeft[x],pointBottom[y],pointLeft[y],bl)

	# Convert coordinate system back
	new_rect = rect = np.array([new_tl, new_tr, new_br, new_bl], dtype = "float32")
	new_rect[:,1] *= -1

	return new_rect

# Derived from https://www.pyimagesearch.com/2014/08/25/4-point-opencv-getperspective-transform-example

def four_point_transform(image, pts):
	# Unpack points
	rect = pts
	(tl, tr, br, bl) = rect

	# compute the width of the new image, which will be the
	# maximum distance between bottom-right and bottom-left
	# x-coordinates or the top-right and top-left x-coordinates
	widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
	widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
	maxWidth = max(int(widthA), int(widthB))

	# compute the height of the new image, which will be the
	# maximum distance between the top-right and bottom-right
	# y-coordinates or the top-left and bottom-left y-coordinates
	heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
	heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
	maxHeight = max(int(heightA), int(heightB))

	# now that we have the dimensions of the new image, construct
	# the set of destination points to obtain a "birds eye view",
	# (i.e. top-down view) of the image, again specifying points
	# in the top-left, top-right, bottom-right, and bottom-left
	# order
	dst = np.array([
		[0, 0], #tl
		[maxWidth - 1, 0], #tr
		[maxWidth - 1, maxHeight - 1], #br
		[0, maxHeight - 1]], #bl
		dtype = "float32")

	# Move image to positive coordinates
	min_x =  round(abs(np.min(rect[:,0])))
	min_y = round(abs(np.min(rect[:,1])))
	T = np.matrix( [[ 1 , 0 , min_x], # Get min x
					[ 0 , 1 , min_y ], # Get min y
					[ 0 , 0 ,    1    ]],
					dtype = "float32")

	# compute the perspective transform matrix and then apply it
	M = cv2.getPerspectiveTransform(rect, dst)
	warped = cv2.warpPerspective(image, T * M  , (maxWidth + min_x , maxHeight + min_y), borderMode=cv2.BORDER_TRANSPARENT)
	# return the warped image
	return warped

# Open Image
img = cv2.imread('img\\example1.jpeg')

# Open windows for control, original image, and result
cv2.namedWindow('Control', cv2.WINDOW_AUTOSIZE)
cv2.namedWindow('Main', cv2.WINDOW_AUTOSIZE)
cv2.namedWindow('Birds Eye', cv2.WINDOW_AUTOSIZE)

# Track bars for coordinates
cv2.createTrackbar( 'X L Bot', 'Control', 0, img.shape[1], onTrackbarChange )
cv2.createTrackbar( 'Y L Bot', 'Control', img.shape[0], img.shape[0], onTrackbarChange )

cv2.createTrackbar( 'X L Top', 'Control', 0, img.shape[1], onTrackbarChange )
cv2.createTrackbar( 'Y L Top', 'Control', 0, img.shape[0], onTrackbarChange )

cv2.createTrackbar( 'X R Top', 'Control', img.shape[1], img.shape[1], onTrackbarChange )
cv2.createTrackbar( 'Y R Top', 'Control', 0, img.shape[0], onTrackbarChange )

cv2.createTrackbar( 'X R Bot', 'Control', img.shape[1], img.shape[1], onTrackbarChange )
cv2.createTrackbar( 'Y R Bot', 'Control', img.shape[0], img.shape[0], onTrackbarChange )

# Loop
while(1):
	# Get Track Bar positions
    pts = np.array(eval('[(' + str(cv2.getTrackbarPos('X L Bot','Control')) + ',' + str(cv2.getTrackbarPos('Y L Bot','Control')) + '),' +
                        '(' + str(cv2.getTrackbarPos('X L Top','Control')) + ',' + str(cv2.getTrackbarPos('Y L Top','Control'))+ '),' +
                        '(' + str(cv2.getTrackbarPos('X R Top','Control')) + ',' + str(cv2.getTrackbarPos('Y R Top','Control'))+ '),' +
                        '(' + str(cv2.getTrackbarPos('X R Bot','Control')) + ',' + str(cv2.getTrackbarPos('Y R Bot','Control'))+ ')]'
                        ), dtype = "int32")

	# Draw the perspective
    imgConnectedPoints = cv2.polylines(img.copy(), [pts], isClosed = True, color = (0,255,0), thickness = 3)
    cv2.imshow('Main',imgConnectedPoints)

	# Draw the transformed bird's eye view
    warped = four_point_transform(img, expandPerspective(order_points(pts), img.shape[1], img.shape[0]))
    cv2.imshow('Birds Eye',warped)

	# Exit
    if cv2.waitKey(1)==27:
        exit(0)

cv.detroyAllWindows()


from django import forms
from .models import Meme

class MemeForm(forms.ModelForm):
    class Meta():
        model = Meme
        fields = ('description','category','meme_img')
        widgets = {
            'description': forms.TextInput(attrs={
                'class': 'field',
                'placeholder': 'Enter Description'
                }),
            'category': forms.Select(choices=model.CATEGORY_CHOICES, attrs={
                'class': 'choice-control',
                'placeholder': 'Choose category',
            }),
            'meme_img': forms.FileInput(attrs={
                'class': 'upload-control',
                'placeholder': 'Choose file',
            })
        }


import matplotlib.pyplot as plt
from sklearn.datasets.samples_generator import make_blobs
import numpy as np

def sigmoid(x):
	return 1.0 / (1 + np.exp(-x))

def dataset():
    (X, y) = make_blobs(n_samples=250, n_features=2, centers=2,
    	cluster_std=1.05, random_state=20)
    X = np.c_[np.ones((X.shape[0])), X]
    return X,y

def initialize_weights(p):
    return np.random.uniform(size = p)

def make_predictions(X,W,link):
    return link(X.dot(W))

def cross_entropy(y,preds):
    y = np.array([y])
    return -np.sum(y*np.log(preds)+(1-y)*np.log(1-preds))/y.shape[0]

def compute_gradient(preds,X,y,cost=cross_entropy,link = sigmoid):
    y = np.array([y])
    if cost == cross_entropy and link == sigmoid:
        return X.T.dot(preds-y)/y.shape[0]

def sgd(X,y,cost = cross_entropy,link=sigmoid,
                     alpha = 0.01,eps = 0.0001, maxit = 1000):
    W = initialize_weights(X.shape[1])
    n = X.shape[0]
    ind = np.random.permutation(np.arange(n))
    X = X[ind]
    y = y[ind]
    i = 0
    losses = []
    preds = make_predictions(X[i:i+1,:],W,link)
    losses.append(cost(y[i],preds))
    it = 0
    while True:
        it += 1
        print("Iteration n.{}".format(it))
        gradient = compute_gradient(preds,X[i:i+1,:],y[i],cost,link)
        W -= alpha*gradient
        i = i + 1
        if i == n:
            ind = np.random.permutation(np.arange(n))
            X = X[ind]
            y = y[ind]
            i = 0
        preds = make_predictions(X[i:i+1,:],W,link)
        l_new = cost(y[i],preds)
        losses.append(l_new)
        if it == maxit:
            break
        """if it > 250 and abs(l_new-losses[it-250])<eps:
            break"""
    return W,losses

#generate the data
X,y = dataset()

# plot the points
plt.scatter(X[:, 1], X[:, 2], marker="o", c=y)

theta,losses = sgd(X,y,cost = cross_entropy,link = sigmoid,
                     alpha = 0.01,eps = 0.0001, maxit = 1000)

Y = (-theta[0] - (theta[1] * X)) / theta[2]

plt.plot(X, Y, "r-")

# construct a figure that plots the loss over time
fig = plt.figure()
plt.plot(np.arange(0, len(losses)), losses)
fig.suptitle("Training Loss")
plt.xlabel("Epoch #")
plt.ylabel("Loss")
plt.show()

AdaGrad, ADAM and AMSGrad/Codes/my_sgd.py


# This code is based on https://github.com/openai/guided-diffusion
"""
Train a diffusion model on images.
"""

import os
import json
from mdm_utils.fixseed import fixseed
from mdm_utils.parser_util import train_args
from mdm_utils import dist_util
from train_utils.train_loop import TrainLoop

from mdm_utils.model_util import create_model_and_diffusion
from train_utils.train_platforms import ClearmlPlatform, TensorboardPlatform, NoPlatform  # required for the eval operation
from train_utils.ted_loader import build_dataloader
def main():
    args = train_args()
    save_dir = f"{args.save_dir}/{args.exp}"
    args.save_dir = save_dir
    print("save_dir:", save_dir)
    fixseed(args.seed)
    train_platform_type = eval(args.train_platform_type)
    train_platform = train_platform_type(args.save_dir)
    train_platform.report_args(args, name='Args')

    args_path = os.path.join(args.save_dir, 'args.json')
    with open(args_path, 'w') as fw:
        json.dump(vars(args), fw, indent=4, sort_keys=True)

    dist_util.setup_dist(args.device)

    print("creating data loader...")
    data = build_dataloader('train', args, shuffle = True)
    print("creating model and diffusion...")
    lang_model = data.dataset.lang_model
    args.lang_model = lang_model
    model, diffusion = create_model_and_diffusion(args, '')

    model.to(dist_util.dev())

    print('Total params: %.2fM' % (sum(p.numel() for p in model.parameters_wo_clip()) / 1000000.0))
    print("Training...")
    TrainLoop(args, train_platform, model, diffusion, data).run_loop()

    train_platform.close()

if __name__ == "__main__":
    main()


"""
Classes representing uploaded files.
"""

import errno
import os
from io import BytesIO

from theory.conf import settings
from theory.core.files.base import File
from theory.core.files import temp as tempfile
from theory.utils.encoding import forceStr

__all__ = ('UploadedFile', 'TemporaryUploadedFile', 'InMemoryUploadedFile',
      'SimpleUploadedFile')

class UploadedFile(File):
  """
  A abstract uploaded file (``TemporaryUploadedFile`` and
  ``InMemoryUploadedFile`` are the built-in concrete subclasses).

  An ``UploadedFile`` object behaves somewhat like a file object and
  represents some file data that the user submitted with a form.
  """
  DEFAULT_CHUNK_SIZE = 64 * 2 ** 10

  def __init__(self, file=None, name=None, contentType=None, size=None, charset=None, contentTypeExtra=None):
    super(UploadedFile, self).__init__(file, name)
    self.size = size
    self.contentType = contentType
    self.charset = charset
    self.contentTypeExtra = contentTypeExtra

  def __repr__(self):
    return forceStr("<%s: %s (%s)>" % (
      self.__class__.__name__, self.name, self.contentType))

  def _getName(self):
    return self._name

  def _setName(self, name):
    # Sanitize the file name so that it can't be dangerous.
    if name is not None:
      # Just use the basename of the file -- anything else is dangerous.
      name = os.path.basename(name)

      # File names longer than 255 characters can cause problems on older OSes.
      if len(name) > 255:
        name, ext = os.path.splitext(name)
        ext = ext[:255]
        name = name[:255 - len(ext)] + ext

    self._name = name

  name = property(_getName, _setName)

class TemporaryUploadedFile(UploadedFile):
  """
  A file uploaded to a temporary location (i.e. stream-to-disk).
  """
  def __init__(self, name, contentType, size, charset, contentTypeExtra=None):
    if settings.FILE_UPLOAD_TEMP_DIR:
      file = tempfile.NamedTemporaryFile(suffix='.upload',
        dir=settings.FILE_UPLOAD_TEMP_DIR)
    else:
      file = tempfile.NamedTemporaryFile(suffix='.upload')
    super(TemporaryUploadedFile, self).__init__(file, name, contentType, size, charset, contentTypeExtra)

  def temporaryFilePath(self):
    """
    Returns the full path of this file.
    """
    return self.file.name

  def close(self):
    try:
      return self.file.close()
    except OSError as e:
      if e.errno != errno.ENOENT:
        # Means the file was moved or deleted before the tempfile
        # could unlink it.  Still sets self.file.closeCalled and
        # calls self.file.file.close() before the exception
        raise

class InMemoryUploadedFile(UploadedFile):
  """
  A file uploaded into memory (i.e. stream-to-memory).
  """
  def __init__(self, file, fieldName, name, contentType, size, charset, contentTypeExtra=None):
    super(InMemoryUploadedFile, self).__init__(file, name, contentType, size, charset, contentTypeExtra)
    self.fieldName = fieldName

  def open(self, mode=None):
    self.file.seek(0)

  def chunks(self, chunkSize=None):
    self.file.seek(0)
    yield self.read()

  def multipleChunks(self, chunkSize=None):
    # Since it's in memory, we'll never have multiple chunks.
    return False

class SimpleUploadedFile(InMemoryUploadedFile):
  """
  A simple representation of a file, which just has content, size, and a name.
  """
  def __init__(self, name, content, contentType='text/plain'):
    content = content or b''
    super(SimpleUploadedFile, self).__init__(BytesIO(content), None, name,
                         contentType, len(content), None, None)

  @classmethod
  def fromDict(cls, fileDict):
    """
    Creates a SimpleUploadedFile object from
    a dictionary object with the following keys:
      - filename
      - content-type
      - content
    """
    return cls(fileDict['filename'],
          fileDict['content'],
          fileDict.get('content-type', 'text/plain'))


from typing import Optional

#se verifica daca treeul este symetric
class TreeNode:
     def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right
class Solution:
    def isSymmetric(self, root: Optional[TreeNode]) -> bool:
        if not root or not root.left and not root.right:
            return True
        stack=[root.left,root.right]
        while stack:
            num_items=len(stack)
            for i in range(num_items//2):
                node1=stack.pop()
                node2=stack.pop()
                if node1 and not node2 or node2 and not node1:
                    return False
                elif not node1 and not node2:
                    continue
                elif node1.data!=node2.data:
                    return False
                else:
                    stack.append(node1.left)
                    stack.append(node2.right)
                    stack.append(node1.right)
                    stack.append(node2.left)

        return True
root=TreeNode()
root.data="root"
root.left=TreeNode()
root.left.data = "a"
root.right = TreeNode()
root.right.data = "a"
root.left.left=TreeNode()
root.left.left.data="a"
root.right.right=TreeNode()
root.right.right.data="a"

if Solution.isSymmetric(self=Solution,root=root):
    print("true")
else:
    print("false")


# -*- coding: utf-8 -*-
"""
Created on Tue Jun 23 11:31:08 2020

@author: dkafkes
"""

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

df = pd.read_csv('master_stdev.csv', header = 0, skiprows = list(np.arange(1, 177)))
df.drop(columns = ['Filename'], inplace = True)
df = df.set_index('Unnamed: 0')
#%%

x = df['B:IMINER']
array, bins, patches = plt.hist(x, bins = 100)
plt.title("B:IMINER Standard Deviation Spread")
plt.xlabel("Average Standard Deviation")
plt.ylabel("Log(Files)")
plt.ylim(0.1, 1000)
plt.semilogy()
plt.show()


import sys, os, re
import unittest
from itertools import product as prod

from timeit import Timer
import time
import math

import logging

import numpy as np

from scipy.optimize import fmin, fmin_bfgs

from hydrodiy.stat.transform import BoxCox2
from hydrodiy.data.containers import Vector

from pygme.model import Model, ParameterCheckValueError
from pygme.calibration import Calibration, CalibParamsVector
from pygme.calibration import ObjFunSSE, ObjFunBCSSE, \
                                ObjFunKGE, ObjFunBiasBCSSE
from pygme.calibration import CalibrationExplorationError

from dummy import Dummy, CalibrationDummy, ObjFunSSEargs

BC = BoxCox2()

# Set logger
LOGGER = logging.getLogger('pygme.Calibration')
fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
ft = logging.Formatter(fmt)
sh = logging.StreamHandler(sys.stdout)
sh.setFormatter(ft)
LOGGER.addHandler(sh)

class ObjFunTestCase(unittest.TestCase):

    def setUp(self):
        print('\t=> ObjFunTestCase')

        nval = 1000
        obs = np.random.uniform(0., 1, size=nval)
        idx = np.random.choice(np.arange(nval), nval//100)
        obs[idx] = np.nan
        self.obs = obs

        sim = np.random.uniform(0., 1, size=nval)
        idx = np.random.choice(np.arange(nval), nval//100)
        sim[idx] = np.nan
        self.sim = sim

    def test_print(self):
        of = ObjFunBCSSE(0.2)
        print(of)

        of = ObjFunSSE()
        print(of)

        of = ObjFunKGE()
        print(of)

    def test_SSE(self):
        obs, sim = self.obs, self.sim
        idx = (~np.isnan(obs)) & (~np.isnan(sim))

        of = ObjFunSSE()
        value = of.compute(obs[idx], sim[idx])
        err = self.obs-self.sim
        expected = np.nansum(err*err)
        self.assertTrue(np.allclose(value, expected))

        value = of.compute(obs, sim)
        self.assertTrue(np.isnan(value))

    def test_KGE(self):
        of = ObjFunKGE()
        obs, sim = self.obs, self.sim
        idx = (~np.isnan(obs)) & (~np.isnan(sim))

        value = of.compute(obs[idx], sim[idx])

        obsok, simok = obs[idx], sim[idx]
        bias = np.mean(simok)/np.mean(obsok)
        rstd = np.std(simok)/np.std(obsok)
        corr = np.corrcoef(obsok, simok)[0, 1]
        expected = 1-math.sqrt((1-bias)**2+(1-rstd)**2+(1-corr)**2)
        self.assertTrue(np.allclose(value, expected))

        value = of.compute(obs, sim)
        self.assertTrue(np.isnan(value))

    def test_BCSSE(self):
        ''' test the BCSSE objfun '''
        obs, sim = self.obs, self.sim
        idx = (~np.isnan(obs)) & (~np.isnan(sim))

        for lam, nu in prod([0.1, 0.5, 1., 2], [1e-4, 1e-2, 1]):
            of = ObjFunBCSSE(lam, nu)
            assert of.name == f"BCSSE{lam:0.1f}"

            value = of.compute(obs[idx], sim[idx])

            BC.lam = lam
            BC.nu = nu
            err = BC.forward(obs)-BC.forward(sim)
            expected = np.nansum(err*err)

            self.assertTrue(np.isclose(value, expected))

            value = of.compute(obs, sim)
            self.assertTrue(np.isnan(value))

    def test_BiasBCSSE(self):
        ''' test the BiasBCSSE objfun '''
        obs, sim = self.obs, self.sim
        idx = (~np.isnan(obs)) & (~np.isnan(sim))
        mo = obs[idx].mean()
        ms = sim[idx].mean()

        for lam, nu in prod([0.1, 0.5, 1., 2], [1e-4, 1e-2, 1]):
            of = ObjFunBiasBCSSE(lam, nu)
            assert of.name == f"BiasBCSSE{lam:0.1f}"

            value = of.compute(obs[idx], sim[idx])

            BC.lam = lam
            BC.nu = nu
            err = BC.forward(obs)-BC.forward(sim)

            expected = np.nansum(err*err)*(1+abs(ms-mo)/mo)
            self.assertTrue(np.isclose(value, expected))

            value = of.compute(obs, sim)
            self.assertTrue(np.isnan(value))

class CalibParamsVectorTestCase(unittest.TestCase):

    def setUp(self):
        print('\t=> CalibParamsVectorTestCase')

        config = Vector([])
        nval = 10
        params = Vector(['X{0}'.format(k) for k in range(1, nval+1)],
                    defaults=np.ones(nval), mins=np.zeros(nval), \
                    maxs=np.ones(nval)*5)
        states = Vector(['S{0}'.format(k) for k in range(1, 3)])
        self.model = Model('test', config, params, states, 2, 2)

    def test_default(self):
        ''' Test setting default values '''
        calparams = CalibParamsVector(self.model)

        self.assertTrue(np.all([s1==s2 for s1, s2 in zip(calparams.names, \
                                    self.model.params.names)]))

        self.assertTrue(np.allclose(calparams.defaults, \
                                    self.model.params.defaults))

    def test_errors_infinite(self):
        ''' Test errors for finite values in calibrated params '''
        nval = self.model.params.nval
        cp = Vector(['X{0}'.format(k) for k in range(1, nval+1)])
        try:
            calparams = CalibParamsVector(self.model, cp)
        except ValueError as err:
            self.assertTrue(str(err).startswith('Expected no infinite'))
        else:
            raise ValueError('Problem with error handling')

    def test_errors_funs(self):
        ''' Test errors related to trans2true and true2trans '''
        nval = self.model.params.nval
        cp = Vector(['X{0}'.format(k) for k in range(1, nval+1)])
        cp = Vector(['tX{0}'.format(k) for k in range(1, nval+1)],\
                    defaults=[0]*nval, mins=[-1]*nval, maxs=[1]*nval)

        fun1 = lambda x: 'string1'
        fun2 = lambda x: 'string2'
        try:
            calparams = CalibParamsVector(self.model, cp, fun1, fun2)
        except ValueError as err:
            self.assertTrue(str(err).startswith(\
                                'Problem with trans2true for'))
        else:
            raise ValueError('Problem with error handling')

        fun = lambda x: np.column_stack([x, x])
        try:
            calparams = CalibParamsVector(self.model, cp, fun, fun)
        except ValueError as err:
            self.assertTrue(str(err).startswith(\
                                'Problem with trans2true for'))
        else:
            raise ValueError('Problem with error handling')

    def test_identity(self):
        nval = self.model.params.nval
        cp = Vector(['tX{0}'.format(k) for k in range(1, nval+1)],\
                    defaults=[0]*nval, mins=[-1]*nval, maxs=[1]*nval)

        calparams = CalibParamsVector(self.model, cp)

        for i in range(10):
            val = np.random.uniform(0, 1, nval)
            calparams.values = val
            self.assertTrue(np.allclose(self.model.params.values, val))

            val = np.random.uniform(0, 1, nval)
            calparams.truevalues = val
            self.assertTrue(np.allclose(calparams.values, val))

    def test_common_transform(self):
        nval = self.model.params.nval
        cp = Vector(['tX{0}'.format(k) for k in range(1, nval+1)],\
                    defaults=[0]*nval, mins=[-1]*nval, maxs=[1]*nval)

        for i, trans in enumerate(['exp', 'sinh']):
            calparams = CalibParamsVector(self.model, cp, trans2true=trans)
            if i == 0:
                trans2true = np.exp
                true2trans = np.log
            elif i == 1:
                trans2true = np.sinh
                true2trans = np.arcsinh

            for i in range(10):
                val = np.random.uniform(0, 1, nval)
                calparams.values = val
                self.assertTrue(np.allclose(calparams.truevalues, \
                                                trans2true(val)))
                self.assertTrue(np.allclose(self.model.params.values, \
                                                trans2true(val)))

                val = np.random.uniform(math.exp(-1), 1, nval)
                calparams.truevalues = val
                self.assertTrue(np.allclose(calparams.values, \
                                                true2trans(val)))

    def test_fixed(self):
        nval = self.model.params.nval
        cp = Vector(['tX{0}'.format(k) for k in range(1, nval+1)],\
                    defaults=[0]*nval, mins=[-5]*nval, maxs=[5]*nval)

        # Choose a fixed value below the max value
        x1 = 4
        fixed = {'X1':x1}

        calparams = CalibParamsVector(self.model, cp, fixed=fixed)

        for i in range(10):
            val = np.random.uniform(0, 1, nval)
            calparams.values = val
            val2 = val.copy()
            val2[0] = x1
            self.assertTrue(np.allclose(self.model.params.values, val2))

            val = np.random.uniform(0, 1, nval)
            calparams.truevalues = val
            val2 = val.copy()
            val2[0] = x1
            self.assertTrue(np.allclose(calparams.truevalues, val2))
            self.assertTrue(np.allclose(calparams.values, val2))

class CalibrationTestCase(unittest.TestCase):

    def setUp(self):
        print('\t=> CalibrationTestCase')

        # Create random inputs
        inputs = np.random.exponential(1, (100, 2))

        # Allocate model
        dum = Dummy()
        dum.allocate(inputs, 2)

        # Run model to create a sudo obs
        params = dum.params.defaults+0.1
        dum.params.values = params
        dum.run()
        obs = dum.outputs[:, 0].copy()

        # Store calibration set up
        self.inputs = inputs
        self.params = params
        self.obs = obs
        self.ical = np.arange(10, obs.shape[0])

    def test_calibration_instance_print(self):
        ''' Test printing of calibration object '''
        calib = CalibrationDummy(warmup=10)
        calib.allocate(self.obs, self.inputs)
        str = '{0}'.format(calib)

    def test_calibration_errors(self):
        ''' Test calibration errors '''
        inputs = np.random.uniform(0, 1, (1000, 2))
        obs = np.random.uniform(0, 1, 1000)

        cp = Vector(['tX1', 'tX2'], mins=[-10]*2, maxs=[10]*2, \
                defaults=[1, 0])
        calparams = CalibParamsVector(Dummy(), cp, trans2true='exp')
        calib = Calibration(calparams)

        try:
            plib = calib.paramslib
        except ValueError as err:
            self.assertTrue(str(err).startswith(\
                    'Trying to access paramslib, but '))
        else:
            raise ValueError('Problem with error handling')

        try:
            calib.ical = obs==obs
        except ValueError as err:
            self.assertTrue(str(err).startswith('Trying to get obs, but '))
        else:
            raise ValueError('Problem with error handling')

    def test_explore(self):
        ''' Test explore function '''
        calib = CalibrationDummy(warmup=10)
        plib = np.random.uniform(-0.1, 0.1, size=(1000, 2)) \
                        + self.params[None, :]
        calib.paramslib = plib

        calib.allocate(self.obs, self.inputs)
        calib.ical = self.ical

        start, _, explo_ofun = calib.explore()
        self.assertTrue(np.allclose(start, self.params, rtol=0., atol=0.05))

    def test_explore_error(self):
        ''' Test calibration exploration error '''

        class ObjFunError(ObjFunSSE):
            ''' Sum of squared error objective function '''

            def __init__(self):
                super(ObjFunError, self).__init__()
                self.name = 'Error'

            def compute(self, obs, sim, **kwargs):
                of = super(ObjFunError, self).compute(obs, sim)
                if of < 1e-1:
                    # This is a stupid error generation
                    # we use it just for testing
                    raise ValueError('Error in exploration')
                return of

        calib = CalibrationDummy(warmup=10, objfun=ObjFunError())
        plib = np.random.uniform(-0.1, 0.1, size=(1000, 2)) \
                        + self.params[None, :]
        calib.paramslib = plib

        calib.allocate(self.obs, self.inputs)
        calib.ical = self.ical

        start, _, explo_ofun = calib.explore()
        # Check that no objective function is below 1e-1
        # because the objective function does not allow it
        self.assertTrue(np.all(explo_ofun > 1e-1))

        # Check that we trigger an error during exploration
        try:
            start, _, explo_ofun = calib.explore(raise_error=True)
        except CalibrationExplorationError as err:
            self.assertTrue(str(err).startswith('Error in explo'))
        else:
            raise ValueError('Problem with error handling')

    def test_explore_fit(self):
        ''' Test explore and fit functions '''
        calib = CalibrationDummy(warmup=10)
        calib.allocate(self.obs, self.inputs)
        calib.ical = self.ical

        start, _, _ = calib.explore()
        final, _, _ = calib.fit(iprint=10,
                                    maxfun=100000, ftol=1e-8)
        ck = np.allclose(calib.model.params.values, self.params, \
                            atol=1e-3, rtol=0.)
        self.assertTrue(ck)

    def test_fit_args(self):
        ''' Test passing arguments to objective function '''
        kwargs = {'lam':1.0, 'idx':np.arange(len(self.ical))}
        calib = CalibrationDummy(objfun=ObjFunSSEargs(), \
                    warmup=10, \
                    objfun_kwargs=kwargs)
        calib.allocate(self.obs, self.inputs)
        calib.ical = self.ical

        start, _, _ = calib.explore()
        final, _, _ = calib.fit(iprint=10,
                                    maxfun=100000, ftol=1e-8)
        ck = np.allclose(calib.model.params.values, self.params, \
                            atol=1e-3, rtol=0.)

        self.assertTrue(ck)

    def test_checkvalues(self):
        def fun(values):
            if values[1] < 0.5:
                raise ParameterCheckValueError

        calib = CalibrationDummy(warmup=10, checkvalues=fun)
        calib.allocate(self.obs, self.inputs)
        calib.ical = self.ical

        start, _, ofuns = calib.explore()

        idx = calib.paramslib[:, 1] < 0.5
        self.assertTrue(np.all(np.isinf(ofuns[idx])))

    def test_fixed(self):
        ''' Test calibration with fixed parameters '''
        # Test error
        fixed = {'X10':self.params[0]+3}
        try:
            calib = CalibrationDummy(warmup=10, fixed=fixed)
        except ValueError as err:
            self.assertTrue(str(err).startswith('Expected names '+\
                                'of fixed parameters'))
        else:
            raise ValueError('Problem with error handling')

        fixed = {'X1':self.params[0]+3}
        calib = CalibrationDummy(warmup=10, fixed=fixed)
        calib.allocate(self.obs, self.inputs)
        calib.ical = self.ical

        start, _, _ = calib.explore()
        final, _, _ = calib.fit(iprint=10,
                                    maxfun=100000, ftol=1e-8)

        self.assertEqual(fixed, calib.fixed)
        self.assertTrue(np.allclose(fixed['X1'], start[0]))
        self.assertTrue(np.allclose(fixed['X1'], final[0]))
        self.assertTrue(np.allclose(fixed['X1'], \
                            calib.model.params.values[0]))

    def test_workflow(self):
        ''' Test calibration workflow (i.e. explore+fit) '''
        calib = CalibrationDummy(warmup=10)

        # Check parameter are not close at the beginning
        ck = ~np.allclose(calib.model.params.values, self.params)
        self.assertTrue(ck)

        # Run calibration
        calib.workflow(self.obs, self.inputs, self.ical, iprint=0,
                maxfun=100000, ftol=1e-8)

        # Test parameters at the end
        ck = np.allclose(calib.model.params.values, self.params, \
                            atol=1e-5, rtol=0.)
        self.assertTrue(ck)

    def test_customised_objfun(self):
        ''' Test customised objective function '''
        # Define a customized objective function
        objfun = ObjFunBCSSE(lam=0.8, nu=1e-5)

        # Instanciate a new calib obj and applies objfun
        calib = CalibrationDummy(warmup=10, objfun=objfun)

        # Check parameter are not close at the beginning
        ck = ~np.allclose(calib.model.params.values, self.params)
        self.assertTrue(ck)

        # Run calibration
        calib.workflow(self.obs, self.inputs, self.ical, iprint=0,
                maxfun=100000, ftol=1e-8)

        # Test parameters at the end
        ck = np.allclose(calib.model.params.values, self.params, atol=1e-3)
        self.assertTrue(ck)

    def test_optimizers(self):
        ''' Test a range of optimizer from scipy '''
        calib = CalibrationDummy(objfun=ObjFunSSE(), \
                    warmup=10)
        calib.allocate(self.obs, self.inputs)
        calib.ical = self.ical
        start, _, _ = calib.explore()

        for iopt, opt in enumerate([fmin, fmin_bfgs]):
            if opt.__name__ in ['fmin', 'fmin_powell']:
                kwargs = dict(maxfun=100000, ftol=1e-8)
            else:
                kwargs = dict(maxiter=100000, gtol=1e-8)

            final, _, _ = calib.fit(start=start, iprint=10, optimizer=opt, \
                                            **kwargs)
            ck = np.allclose(calib.model.params.values, self.params, \
                    atol=5e-3, rtol=0.)
            if not ck:
                print(('Failing optimizer test {0} '+\
                    'expected params={1}, got {2}').format(\
                        iopt+1, \
                        ' '.join(list(np.round(\
                                self.params, 2).astype(str))), \
                        ' '.join(list(np.round(\
                                calib.model.params.values, 2).astype(str)))
                ))

            self.assertTrue(ck)

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


import pdb
import unittest
import json

from objbrowser import browse
import mock
from mock import patch

import music_server
from music_server import youtube_search
from music_server import config

class YoutubeSearchTestCase(unittest.TestCase):

    def setUp(self):
        pass
    def tearDown(self):
        pass

    def test_format_query(self):
        # given
        search_query = "simple_query"
        expected_result = "http://youtube.com/results?search_query=simple_query"
        # when
        query = youtube_search.format_query(search_query)
        # then
        self.assertEqual(query, expected_result)

    def test_format_query_with_space(self):
        # given
        search_query = "a b"
        expected_result = "http://youtube.com/results?search_query=a+b"
        # when
        query = youtube_search.format_query(search_query)
        # then
        self.assertEqual(query, expected_result)

    def test_format_with_plus(self):
        # given
        search_query = "a+b"
        expected_result = "http://youtube.com/results?search_query=a%2Bb"
        # when
        query = youtube_search.format_query(search_query)
        # then
        self.assertEqual(query, expected_result)

    def test_fetch_first_result_when_empty(self):
        self.assertRaises(TypeError, youtube_search.fetch_results, None)

    # empty list
    def test_fetch_first_result_when_no_result(self):
        # given
        html_content = "wrong html content"
        # when
        result = youtube_search.fetch_results(html_content)
        # then
        self.assertFalse(result, 'Result should be an empty list')

    def test_fetch_results(self):
        # given
        with open(config.test_resources_folder + 'youtube_search_pratos_osni.html', 'r') as myfile:
            html_content = myfile.read()
        with open(config.test_resources_folder + 'youtube_search_pratos_osni.json', 'r') as myfile2:
            expected_links = json.loads(myfile2.read())
        # when
        results = youtube_search.fetch_results(html_content)
        # then
        self.assertEqual(results, expected_links)

    @patch('music_server.youtube_search.get_html')
    def test_youtube_search(self, test_patch):
        # given
        with open(music_server.config.test_resources_folder + 'youtube_search_pratos_osni.html') as fh:
            mock_html = fh.read()
            test_patch.return_value = mock_html
        with open(config.test_resources_folder + 'youtube_search_pratos_osni.json', 'r') as myfile2:
            expected_links = json.loads(myfile2.read())
        # when
            results = youtube_search.YoutubeSearch("pratos osni").video_ids
        # then
        self.assertEqual(results, expected_links)

    def test_search_empty(self):
        # given
        search_query = ''
        # when
        results = youtube_search.YoutubeSearch(search_query)
        # then
        self.assertTrue(results)

    def test_search_none(self):
        # given
        search_query = None
        # when
        results = youtube_search.YoutubeSearch(search_query)
        # then
        self.assertTrue(results)

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