#!/usr/bin/env python3
# [Model] Unit Test Template (non-search)
# Authors:
#     Logan Bateman
#     Dakota Cookenmaster
#     Jesse Hines

from trello import TrelloClient
import re
import json
import os
import subprocess

TEST_FOLDER = os.path.dirname(os.sys.argv[0])

if os.path.exists(f'{TEST_FOLDER}/trello-api.json'):
    with open(f'{TEST_FOLDER}/trello-api.json') as file:
        params = json.load(file)
        client = TrelloClient(
            api_key=params['api_key'],
            api_secret=params['api_secret'],
            token=params['token'],
        )
else:
    print(f"""Add {TEST_FOLDER}/trello-api.json and run again
{{
  "api_key": "your key here",
  "api_secret": "your secret here",
  "token": "your token here"
}}
""")
    exit(1)

FIXTURE_DATA_PATH               = f'{TEST_FOLDER}/fixtures/data'
MODEL_FOLDER                    = f'{TEST_FOLDER}/../models'
PHP_FIXTURE_JSON_CONVERTER      = f'{TEST_FOLDER}/fixture_to_json.php'
SHARE_HIM_BOARD                 = client.get_board('5f03979153257d3ed6e7b85e')
cards                           = SHARE_HIM_BOARD.get_cards()

#def apply_template(template, **params):
#    for name, value in params.items():
#        template = template.replace("{" + name + "}", value)
#    return template

def capitalizeFirst(word):
    return word[0].upper() + word[1::]

def decapitalizeFirst(word):
    return word[0].lower() + word[1::]

def camel_to_snake(camel_string):
    #https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case
    snake_string = re.sub(r'(?<!^)(?=[A-Z])', '_', camel_string).lower()
    return snake_string

def fixture_data_dict(model_name):
    result = subprocess.run(['php', PHP_FIXTURE_JSON_CONVERTER, f'{FIXTURE_DATA_PATH}/{get_table_name_from_model(f"{MODEL_FOLDER}/{model_name}.php")}.php'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    data = json.loads(result.stdout)
    return data

def get_table_name_from_model(model_path):
    model_php = open(model_path).read()
    table_search = re.search(r'{{%([^}]*)}}', model_php)
    if table_search:
        return table_search.group(1)
    else:
        return ''

def template_fixture_use(model_name):
    return fr'use app\tests\fixtures\{model_name}Fixture;'

def template_fixture_function(model_name):
    return f'''    public function _fixtures()
    {{
        return [
            '{model_name}' => [
                'class' => {model_name}Fixture::class,
            ]
        ];
    }}'''

def template_search_use(model_name):
    return fr'use app\models\{model_name}Search;'

def template_search(model_name):
    return fr"""    public function testSearch()
    {{
        $this->tester->assertSearchReturns(new {model_name}Search(), [
                "replaceme" => replaceme,
            ],
            [/* expected ids... */]
        );
    }}"""


def template_create(params):
    data = fixture_data_dict(params['model_name'])

    if data:
        rec = list(data.values())[0]
        lines = str()
        for key, val in rec.items():
            lines += f'        $model->{key} = \'{val}\';\n'
        return fr"""
    /**
     * Test creating a new {params["model_name"]}
     */
    public function testCreate{params["model_name"]}()
    {{
        $model = new {params["model_name"]}();
{lines}     
        $isSaved = $model->save();
        
        if ($isSaved) {{
            codecept_debug('## Model saved: ' . print_r($model, true));
        }} else {{
            codecept_debug($model->getErrors());
            codecept_debug('## Failed to save model: ' . print_r($model, false) . "\n" .
                \yii\helpers\Html::errorSummary($model, ['class' => 'errors'])
            );
        }}

        $this->tester->assertTrue($isSaved, true);
        $this->tester->seeRecord('app\models\{params["model_name"]}', ['{decapitalizeFirst(params["model_name"])}Id' => $model->{decapitalizeFirst(params["model_name"])}Id]);       
    }}
    """
    else:
        return fr"""
    /**
     * Test creating a new {params["model_name"]}
     */
    public function testCreate{params["model_name"]}()
    {{
        $model = new {params["model_name"]}();
        $model->replaceme = replaceme;
        
        $isSaved = $model->save();
        
        if ($isSaved) {{
            codecept_debug('## Model saved: ' . print_r($model, true));
        }} else {{
            codecept_debug($model->getErrors());
            codecept_debug('## Failed to save model: ' . print_r($model, false) . "\n" .
                \yii\helpers\Html::errorSummary($model, ['class' => 'errors'])
            );
        }}

        $this->tester->assertTrue($isSaved, true);
        $this->tester->seeRecord('app\models\{params["model_name"]}', ['{decapitalizeFirst(params["model_name"])}Id' => $model->{decapitalizeFirst(params["model_name"])}Id]);       
    }}
    """

def template_update(params):
    return fr"""
    /**
     * Test updating {model_name}
     */
    public function testUpdate{params["model_name"]}()
    {{
        $this->tester->seeRecord('app\models\{params["model_name"]}', ['{decapitalizeFirst(params["model_name"])}Id' => 1]);
        
        $model = $this->tester->grabRecord('app\models\{params["model_name"]}', ['{decapitalizeFirst(params["model_name"])}Id' => 1]);
        $model->replaceme = 'replaceme (UPDATED)';
        $model->save();
        
        $this->tester->seeRecord('app\models\{params["model_name"]}',     ['replaceme' => 'replaceme (UPDATED)']);
        $this->tester->dontSeeRecord('app\models\{params["model_name"]}', ['replaceme' => 'replaceme']);
    }}
    """

# Function templates
templates = {
    'delete': lambda params: fr"""
    /**
     * Test deleting {params["model_name"]}
     */
    public function testDelete{params["model_name"]}()
    {{
        $this->tester->seeRecord('app\models\{params["model_name"]}', ['{decapitalizeFirst(params["model_name"])}Id' => 1]);
        $model = \app\models\{params["model_name"]}::find()->where(['{decapitalizeFirst(params["model_name"])}Id' => 1])->one();
        $model->delete();
        $this->tester->dontSeeRecord('app\models\{params["model_name"]}', ['{decapitalizeFirst(params["model_name"])}Id' => 1]);
    }}
    """,
    
    'create': template_create,
    
    'view': lambda params: fr"""
    /**
     * Test viewing {model_name}
     */
    public function testView{params["model_name"]}()
    {{
        $this->tester->seeRecord('app\models\{params["model_name"]}', ['{decapitalizeFirst(params["model_name"])}Id' => 1]);
    }}
    """,
    
    'update': template_update,

    'returnValueIsNotNull': lambda params: fr"""
    public function testReturnValueIsNotNull()
    {{
        $model = new {params["model_name"]}();
        {''.join(f'''
        $this->assertTrue( $model->{func}() !== null );''' for func in params["params"])}
    }}
    """
}
def default_template(params):
    return fr"""
    public function test{params["method_name"]}()
    {{
        $model = new {params["model_name"]}();
    }}
"""

for card in cards:
    if ("[Model]" in card.name) and ("Search" not in card.name):
        model_name = card.name.replace("[Model]", "").strip()
        print(f"Making {model_name}Test.php")
        with open(f"{TEST_FOLDER}/unit/models/{model_name}Test.php", "w+") as file:
            tests = {} # dictionary of tagNames to paramaters.
            for checklist in card.checklists:
                if checklist.name == "To Do":
                    for item in checklist.items:
                        tag = re.search("\[(.*?)\]", item['name']).group(1)
                        tagName, _, tagParam = re.search("(\w*)(\((\w*)\))?", tag).groups()
                        tagParam = [] if not tagParam else [tagParam]
                        if tagName in tests:
                            tests[tagName]["params"].extend(tagParam)
                        else:
                            tests[tagName] = {"model_name": model_name, "method_name": capitalizeFirst(tagName), "params": tagParam}

            # Evaluate all the templates.
            tests = [templates.get(tagName, default_template)(params) for tagName, params in tests.items()]

            has_fixture = True if fixture_data_dict(model_name) else False
            has_search = os.path.exists(f"{MODEL_FOLDER}/{model_name}Search.php")

            file.write(fr'''<?php
namespace tests\unit\models;

use app\models\{model_name};
{template_search_use(model_name) if has_search else str()}
{template_fixture_use(model_name) if has_fixture else str()}

class {model_name}Test extends \Codeception\Test\Unit
{{
{template_fixture_function(model_name) if has_fixture else str()}
{"".join(tests)}
{template_search(model_name) if has_search else ''}
}}
'''  
)
