package com.ftinc.si.assist.run;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;

import com.ftinc.si.assist.test.AssertRecord;
import com.ftinc.si.assist.test.TestCaseRecord;
import com.ftinc.si.assist.test.TestCommandRecord;
import com.ftinc.si.assist.test.TestLogger;
import com.ftinc.si.assist.test.Tool;
import com.ftinc.si.assist.test.gui.Plugin;

public class TestCommander {
	private static int count_try = 0;

	private ArrayList<TestCommandRecord> m_cmdlist = null;
	private TestCaseRecord m_rec;//実行するTestCase

	//TestCaseRecordのsnapshotが0でもincludeしているTestCaseRecordにsnapshotがあれば、それが有効になる。
	private static int s_snapshot = 0;

	private boolean m_enableAssert;//コンストラクタの最終引数。includeでfalse指定。

	private String m_idPrefix = ""; //$NON-NLS-1$
	private static String m_version = ""; //$NON-NLS-1$
	public boolean is_stopping = false;//Toolから書き換えられることもある。Exitの代わり。Jsonの失敗など。

	public ArrayList<String> m_fakeStack;
	public String m_lastCallofFake = "";

	//false:全てのTestCommandを実行する。 true:automaticを保証されたTestCommandのみを実行する。
	private static boolean only_auto = false;

	//別プロセスとして起動される場合に呼ばれる。
	//第一引数はeditmode
	//第二引数はTestCaseを表現するString[]のJSON形式
	public static void runCase(HashMap<String, Object> args) {
		if (args.size() >= 2) {
			String x_version = Tool.version;//一時的にversionを変更する可能性があるので保存する。

			//環境設定を読み込み、Toolなどの初期化を行う。executeでも呼ぶが設定済みならすぐに返るので問題ない。
			Boolean edit_mode = (Boolean)args.get("editmode"); //$NON-NLS-1$
			if (edit_mode == null) {
				edit_mode = false;//指定がなければ、非編集モード
			}

			Boolean child_mode = (Boolean)args.get("child_mode");//$NON-NLS-1$
			if (child_mode != null && child_mode) {
				//子プロセスの内部なのでフラグを立てる。
				Tool.child_mode = true;
			}

			Tool.init(edit_mode);//インスタンステーブルと編集モードの設定

			if (args.containsKey("v")) { //$NON-NLS-1$
				//版数指定
				m_version = args.get("v").toString(); //$NON-NLS-1$
				Tool.version = m_version;//DBアクセス時にTool.versionにアクセスするので。
			} else {
				m_version = Tool.version;
			}
			if (args.containsKey("onlyauto")) {
				only_auto = Boolean.valueOf(args.get("onlyauto").toString());
			}

			try {
				String t_arg = args.get("caseName").toString().replaceAll("'", "\"");//'を"に戻す。 //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

				ArrayList<TestCaseRecord> t_list = Tool._db().getTestCaseList(t_arg, 0, 0, "", m_version); //$NON-NLS-1$

				if (t_list.size() > 0) {
					ArrayList<String> t_case = new ArrayList<String>();
					t_case.add(edit_mode.toString());

					//検索に掛かった最初のものだけ実行する。同一プロセスで、同一クラスの改変が複数生じるのを防止するため。
					t_case.add(t_list.get(0).getCmdArgsStyle());

					//TestMainから呼ばれた時にprocess_modeが明に指定される。（コマンドラインからの実行）
					execute(t_case);
				} else {
					//ログをTestLoggerから取得し、記録する。
					Tool.logForTesting(null, "TestCase=" + t_arg + Messages.getString("TestCommander.5") + m_version); //$NON-NLS-1$ //$NON-NLS-2$
					if (!Tool.child_mode) {
						Tool.alertMSG(null, "TestCase=" + t_arg + Messages.getString("TestCommander.7") + m_version); //$NON-NLS-1$ //$NON-NLS-2$
					}
				}
			} finally {
				if (Tool.child_mode) {
					//main（プロセス）の終了時にはお掃除。
					//引数のprocessModeがtrueの場合は、外部プロセスから起動された場合のみ。
					Tool.destroy();
					System.exit(0);
				}
				Tool.version = x_version;
			}
		} else {
			//ログをTestLoggerから取得し、記録する。
			Tool.logForTesting(null, Messages.getString("TestCommander.1")); //$NON-NLS-1$
			if (!Tool.child_mode) {
				Tool.alertMSG(null, Messages.getString("TestCommander.2")); //$NON-NLS-1$
			}
		}
	}

	public TestCommander(TestCaseRecord rec, boolean logFlag) {
		m_rec = rec;
		m_enableAssert = logFlag;
		m_fakeStack = new ArrayList<String>();
	}


	//アプリのテストプログラムから呼ばれる可能性があるのでpublicにしておく。
	//その場合、destroyは不要。同一プロセスのため。
	public static void execute(ArrayList<String> args) {
		s_snapshot = 0;
		if (count_try > 0) {
			//非プロセスモードの時に、この経路に入る。
			//同じプロセスで二回以上実行してもtoClassができないので中止。
			Tool.alertMSG(null, Messages.getString("TestCommander.10")); //$NON-NLS-1$
			return;
		}
		count_try++;

		try {
			//[name,description,begin,end,loggable,snapshot,include,groupCode]
			String[] t_data = (String[]) Tool.getObjectfromJSON(String[].class, args.get(1));
			if (t_data == null) {
				Tool.alertMSG(null, args.get(1) + Messages.getString("TestCommander.11")); //$NON-NLS-1$
			} else {
				TestCaseRecord t_rec = new TestCaseRecord(0, t_data);

				//JUnit形式のログのヘッダ
				TestLogger.beginTestSuite(t_rec);

				//テストケース毎にTestCommanderのインスタンスを生成し、実行する。
				TestCommander tester = new TestCommander(t_rec, t_rec.loggable);
				if (t_rec.snapshot > 0) {
					//snapshotが指定されていれば、更新する。
					s_snapshot = t_rec.snapshot;
				}
				Tool.curCommander(tester);

				//preExecからの場合、nullではない。
				tester.execTestCase(null);

				//ログをTestLoggerから取得し、記録する。
				Tool.logForTesting(null, TestLogger.endTestSuite());
			}
		} catch (Exception e) {
			//ログをTestLoggerから取得し、記録する。
			e.printStackTrace();
			Tool.logForTesting(e, "Exception=" + e.getClass().getName() + " message=" + e.getMessage() + " Case=" + Tool.curCase + " ID=" + Integer.toString(Tool.curID)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
		} finally {
			//結果を記録する。プロセスモードであれば、System.outに結果を出す。
			//processmodeでなければ、ファイルに書き込む。
			Tool.displayResult(null);

			//TestCaseごとにクリアする。process_modeを元に戻す。
			Tool.clearCase();
		}
	}


	//指定したTestCaseのうち実行可能なものをすべて実行。
	//snapshotid: TestCaseRecordに指定されていた場合、TestCommandRecord.preExec()からの呼び出し。
	//            Snapshotの再生であるならば、同じ処理をsnapshotまで行い、状態を復元し、必要な属性を変更してから、実行する。
	public void execTestCase(Integer snapid) {
		FakeEditorOnRun.reset();	//編集モードで、スキップすると決めたメソッド一覧を初期化

		//ローカル環境でinclude先の別名を指定されていたら変更する。
		String inc_name = Tool.convert2Alias(m_rec.include); //$NON-NLS-1$

		//includeを先に実行する。
		if (inc_name.length() > 0 && !"NONE".equals(inc_name)) { //$NON-NLS-1$
			//カンマ区切りで複数のTestCaseをincludeできるため。
			String[] inc_names = inc_name.split(",");
			for (int i = 0; i < inc_names.length; i++) {
				//includeの名前と属性を操作するためのjsonを分離する。

				//同一versionのTestCaseのみを実行する。
				ArrayList<TestCaseRecord> t_inc = Tool._db().getTestCaseList(inc_names[i], 0, 0, "", m_version); //$NON-NLS-1$
				if (t_inc.size() == 0) {
					Tool.alertAndExit("TestCase=\"" + inc_names[i] + "\" not exists." , null, 0); //$NON-NLS-1$ //$NON-NLS-2$
					return;
				}

				TestCaseRecord inc_rec = t_inc.get(0);

				if (!m_rec.loggable) {
					//現在のTestCaseがログ出力しない方針なら、それはincludeにも及ぶ。
					inc_rec.loggable = false;
				}

				TestCommander tester = new TestCommander(inc_rec, false);//assertしない。
				//icludeされたテストケースの結果を識別
				if (m_idPrefix.length() > 0) {
					tester.m_idPrefix = m_idPrefix + "."; //$NON-NLS-1$
				}
				tester.m_idPrefix +=  m_rec.name;
				tester.execTestCase(inc_rec.snapshot);

				if (inc_rec.snapshot > 0) {
					//snapshotが指定されていれば、更新する。
					//※includeの中でincludeしている可能性があるので、execTestCaseの後で設定する。
					s_snapshot = inc_rec.snapshot;
				}

				if (is_stopping) {//途中のダイアログで中止が選択された場合
					return;
				}
			}
		}

		//include後のコマンドを実行する。
		ArrayList<TestCommandRecord> t_casecmdlist = Tool._db().getTestCmdList(m_rec.name, null, null, m_version);//クラス名、メソッド名は指定しない。
		m_cmdlist = TestCommandRecord.convertToObject(t_casecmdlist, m_rec.snapshot);

		if (m_cmdlist != null) {
			for (int j = 0; j < m_cmdlist.size(); j++) {
				TestCommandRecord t_rec = m_cmdlist.get(j);
				if (s_snapshot > 0) {
					//includeから含めて、Snapshotが指定されていれば、フラグを立てる。
					SnapshotCommandRecord.s_id_snapshot = s_snapshot;
				} else {
					//staticなので影響を残さないように元に戻す。
					SnapshotCommandRecord.s_id_snapshot = null;
				}
				String prev_name = Tool.curCase;
				int prev_id = Tool.curID;

				//コマンドの実行と結果の評価:　各プラグインがTestCommanRecordを拡張するなら、ここで割込を入れる。
				execCommand(setCommandIfExtended(t_rec));
				if (snapid != null && snapid == t_rec.id) {
					//元に戻す。
					Tool.setCurID(prev_name, prev_id);

					//snapshotの状態が復元したので、preExecを抜ける。
					return;
				}

				if (is_stopping) {//途中のダイアログで中止が選択された場合
					return;
				}
			}
		}
	}


	//TestCommandRecordのサブクラスを指定したいときのため、プラグインからの割り込み口を作る。
	private TestCommandRecord setCommandIfExtended(TestCommandRecord rec) {
		//各pluginに変更するかどうか聞く
		for (int k = 0; k < Tool.plugins.size(); k++) {
			Plugin t_plug = Tool.plugins.get(k);

			TestCommandRecord t_rec = (TestCommandRecord) t_plug.extendCommand(rec);
			if (t_rec != null) {
				//PluginはTestCommandRecordを加工できる。{
				return t_rec;
			}
		}
		return rec;
	}

	//sbapshotからのアクセスのためprotectedとする。
	private void execCommand(TestCommandRecord rec) {
		//デバッグ用の手がかりのための初期化
		m_lastCallofFake = null;
		m_fakeStack = new ArrayList<String>();

		//テスト数のカウント。準備段階なのでカウントアップしない。実行前までにエラーが出る場合のための措置。
		TestLogger.cmdPrepare(rec.getName4Log());
		if (!rec.enable) {
			//非活性の場合、何もしない。
			TestLogger.cmdDisabled(0, m_idPrefix);
			return;
		} else if (!rec.isComplete()) {
			//未完成のコマンドは実行しない。
			//　未完成とは、引数のjsonが不正、もしくはAssertの判定条件、正規表現が不正の場合である。
			TestLogger.cmdSkipped(0, m_idPrefix);
			return;
		}
		//事前実行する処理があれば行う。snapshotの場合、その直前までを行う。ログは出さない。
		rec.preExec();

		//Toolに現在位置を教える。preExecの後にするのは、preExcecでもexecCommandを実行するため。
		Tool.setCurID(m_rec.name, rec.id);

		//以下、実行する。
		Method t_verb = null;
		Object s_obj = null;
		Class<?>[] arg_classes = null;;
		int phase = 0;
		try {
			arg_classes = Tool.getArgClasses(rec);
			phase++;

			//コンストラクタでない場合、$0をセット。
			if (!rec.className.equals(rec.getMethodName())) {
				//Snapshotは、下の呼び出しの末端のTestCommandRecordの中で行う。
				s_obj = rec.getTObject(0);//staticなら、nullになる。

				if (s_obj == null && !rec.isStatic()) {
					TestLogger.cmdCauseError(0, Integer.toString(rec.id) + ", @new=" + rec.className + "\n", m_idPrefix);
					Tool.alertAndExit("@execMethod $0 is null and the method is not static." , null, rec.id); //$NON-NLS-1$ //$NON-NLS-2$
				} else if (rec.isStatic()) {
					s_obj = null;//getTObjectでsnapshotや大域変数の処理は終わっているため、明にnullにする。
				}
				phase++;
			}

		} catch (Exception e) {
			String[] t_msg = {"failed to getTObject", "wrong argument types", "failed to identify method"}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			TestLogger.cmdCauseError(0, Integer.toString(rec.id) + Messages.getString("TestCommander.19") + " " + e.getClass().getSimpleName() + ", message=" + Arrays.toString(t_msg) + "\n", m_idPrefix, e.getCause()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			Tool.alertAndExit("@execMethod " + t_msg[phase] + ": ", e, rec.id); //$NON-NLS-1$ //$NON-NLS-2$
		}
		Object _ret = null;
		Object[] t_objs = null;
		boolean assert_skipped = false;
		try {
			//Snapshotが指定されていれば、保存は下の呼び出しの末端のTestCommandRecordのgetTObjectで行う。
			t_objs = getArgObjs(rec);
		} catch (Exception e) {
			//標的のメソッド以外から投げられたException。
			String eclass = e.getClass().getName();
			String msg = e.getMessage();
			if (msg == null && e.getCause() != null) {
				eclass = e.getCause().getClass().getName();
				msg = e.getCause().getMessage();
			}
			TestLogger.cmdCauseError(0, Integer.toString(rec.id) + Messages.getString("TestCommander.19") + eclass + ", message=" + msg + "\n", m_idPrefix, e.getCause()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			return;
		}
		AssertRecord.setStart(new Date());

		try {
			phase = 100;//100以上はConstructor
			Class<?> t_c = Tool.forName(rec.className);

			if (rec.className.equals(rec.getMethodName())) {
				//コンストラクタ
				phase = 120;//100以上はConstructor

				_ret = Tool.newObject(t_c, arg_classes, t_objs);
				phase++;

			} else {
				//メソッド
				phase = 150;//150以上はMethod
				t_verb = t_c.getDeclaredMethod(rec.getMethodName(), arg_classes);

				phase = 151;//100以上はConstructor
				t_verb.setAccessible(true);//テストするメソッドはpublicとは限らないのでアクセスチェックを外す。

				phase = 152;//100以上はConstructor

				//invoke!!!!!!!!!

				if ((!rec.isAutomatic() && only_auto)) {
					//only_autoモードの時、人が介在するテストはスキップする。
					//引数を構築してからなので、このタイミングである。
					TestLogger.cmdSkipped(0, m_idPrefix);
					return;
				}
				if (s_snapshot == rec.id) {
					//このコマンドがsnapshotならば、assertをスキップする。
					assert_skipped = true;
				} else {
					//recの名前が整ったタイミング。引数も初期化したので、ここから本番とする。
					if (m_enableAssert) {
						//ログを取る時だけカウントする。
						TestLogger.cmdBegin(rec.getName4Log());
					}

					_ret = t_verb.invoke(s_obj, t_objs);
				}
				phase++;
				AssertRecord.setEnd(new Date());
			}
		} catch (Error e) {
			//assertしない場合でもエラーは見る。
			AssertRecord.setEnd(new Date());
			evaluateException(phase, rec, e);
			return;
		} catch (Exception e) {
			//assertしない場合でも例外は見る。
			//String xinfo = Tool.getStackMessage(e, 0, 10);
			AssertRecord.setEnd(new Date());
			evaluateException(phase, rec, e);
			return;
		}

		phase = 200;//200以上はAssert
		//instanceの登録.引数や$0はTool.getTObjectの中で登録している。
		if (_ret != null) {
			Tool.addInstance(Integer.toString(Tool.curID) + "$_", Tool.primitive2ToObject(_ret)); //$NON-NLS-1$
		}
		if (s_obj != null) {
			Tool.addInstance(Integer.toString(Tool.curID) + "$0", s_obj); //$NON-NLS-1$
		}
		Tool.addInstanceArgs(Integer.toString(Tool.curID), t_objs, rec.varArgsPos());
		phase++;

		//テスト結果を判定する。
		if (m_enableAssert && !assert_skipped) {
			execAssert(phase, rec);
		}
	}

	//Exceptionがテストしているメソッドで発生した場合、それが合っているかどうか判定する。
	private void evaluateException(int phase, TestCommandRecord rec, Throwable e0) {
		AssertRecord ar = null;
		try {
			ar = rec.getAssert();
		} catch (Exception e) {
			//最後の外部プロセスで例外発生。
			ar = Tool._db().getAssert(rec.id);
			ar.evaluateException(phase, rec, null, e0, e);
			return;
		}

		if (ar == null) {
			TestLogger.cmdSkipped(0, m_idPrefix);
			return;
		} else if (!ar.isCompleted()) {
			//判定が設定されていない、もしくは、主要属性が設定されていて、TargetObjの先頭が「!」の場合、未完成。
			TestLogger.cmdSkipped(ar.getMiliSec(), m_idPrefix);
			return;
		} else if (!ar.enable && !"Exception".equals(ar.criteriaType)) { //$NON-NLS-1$
			//非活性かつException以外を判定する場合なにもしない。予期しないExceptionには対応する。
			TestLogger.cmdSkipped(ar.getMiliSec(), m_idPrefix);
			return;
		}
		ar.m_prefix = m_idPrefix;

		String fake_info = null;
		if (m_fakeStack.size() > 0) {
			fake_info = Messages.getString("TestCommander.0") + m_fakeStack.get(m_fakeStack.size() - 1) + ")"; //$NON-NLS-1$ //$NON-NLS-2$
		} else {
			fake_info = Messages.getString("TestCommander.25") + m_lastCallofFake + ")"; //$NON-NLS-1$ //$NON-NLS-2$
		}
		Throwable t_e = e0;
		if (e0.getCause() != null) {
			t_e = e0.getCause();
		}

		ar.evaluateException(phase, rec, fake_info, t_e);

		//例外が出ない前提かどうかを判定。
		if (ar.criteriaType.equals("Exception")) { //$NON-NLS-1$
			if (ar.criteriaRex.equals("\\$null")) { //$NON-NLS-1$
				//VCentral, CodeFairyの例外と思われる。続行不可能なので抜ける。

				//is_stopping = true;
				//いったん、止めない方式にしてみる。
			}
		}
	}

	// Assertの実行。
	//①指定Exceptionの発行を確認。
	//②ターゲットのオブジェクトが指定の正規表現とマッチすることを確認。
	//③ターゲットのオブジェクトが指定の正規表現とマッチしないことを確認
	private void execAssert(int phase, TestCommandRecord rec)  {
		AssertRecord ar = null;
		try {
			ar = rec.getAssert();
		} catch (Exception e) {
			//最後の外部プロセスで例外発生。
			ar = Tool._db().getAssert(rec.id);
			ar.evaluateException(phase, rec, null, e);
			return;
		}
		if (ar == null) {
			TestLogger.cmdSkipped(0, m_idPrefix);
			return;
		} else if (!ar.isCompleted()) {
			//判定が設定されていない、もしくは、主要属性が設定されていて、TargetObjの先頭が「!」の場合、未完成。
			TestLogger.cmdSkipped(ar.getMiliSec(), m_idPrefix);
			return;
		} else if (!ar.enable && !"Exception".equals(ar.criteriaType)) { //$NON-NLS-1$
			//非活性かつException以外を判定する場合なにもしない。予期しないExceptionには対応する。
			TestLogger.cmdSkipped(ar.getMiliSec(), m_idPrefix);
			return;
		}
		ar.m_prefix = m_idPrefix;

		if (ar.targetObj != null && ar.targetObj.length() > 0 && !ar.criteriaType.equals("Exception")) { //$NON-NLS-1$
			ar.evaluate(phase, rec);
		} else if (ar.criteriaType.equals("Exception")) { //$NON-NLS-1$
			//例外が出ない前提かどうかを判定。
			if (ar.criteriaRex.equals("\\$null")) { //$NON-NLS-1$
				ar.evaluate(phase, rec);
			}
		}
	}

	//クラスの配列から引数のインスタンス配列を得る。
	// ArgType: className+" "+stack（$n or pastid$n）
	// objKey: testid(current)$n or @testid(pst)$n	前者はObjectRecordからインスタンスを生成する。後者は既存オブジェクトを再利用する。
	public Object[] getArgObjs(TestCommandRecord testRec) throws Exception {
		int len = testRec.numArgs();
		int pos = testRec.varArgsPos();
		int n = 0;

		ArrayList<Object> t_list = new ArrayList<Object>();
		try {
			if (pos >= 0) {
				//可変長引数の場合
				String vcname = testRec.getArgClass(pos);
				Class<?> compClass = Tool.forName(vcname);
				Object t_arry = Array.newInstance(compClass, len - pos);
				for (int j = 0; j < len - pos; j++) {
					n = pos + j;
					Object j_obj = testRec.getTObject(pos + j + 1);
					Array.set(t_arry, j, j_obj);
				}

				t_list.add(t_arry);
			} else {
				//可変長ではない
				pos = len;
			}
			for (int i = pos - 1; i >= 0; i--) {//この段階で加える引数の数はlen-posｰ1個
				n = i + 1;
				Object i_obj = testRec.getTObject(i + 1);//最初の引数$1の場合、要素の位置は1。1より小さくならない。
				t_list.add(0, i_obj);
			}
		} catch (Exception e) {
			throw new Exception(Messages.getString("TestCommander.33") + Integer.toString(n) + Messages.getString("TestCommander.34") + e.getMessage()); //$NON-NLS-1$ //$NON-NLS-2$
		}

		return t_list.toArray(new Object[t_list.size()]);
	}

}
