package com.ftinc.si.assist.test;

import java.lang.reflect.InvocationTargetException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.JDialog;
import javax.swing.JOptionPane;

import com.ftinc.si.assist.run.VCentral;
import com.ftinc.si.assist.test.Fson.NotSupportedClassException;

import javassist.CannotCompileException;
import javassist.NotFoundException;

public class AssertRecord extends Record implements Cloneable, AssertIF {
	public AssertRecord() {
		super(0);
	}

	public AssertRecord(int i) {
		super(i);
	}

	public int id;  // TestIDと同じ
	public String targetObj;  // 結果判定に用いるArrayList<String>を作るためのソース、もしくはException名
	public String criteriaType = "Inspect";
	public String criteriaRex = "[]";
	public String description;

	public int snapshot_id = -1;

	//DBの結果を読み込む
	public void read(ResultSet r) throws SQLException {
		targetObj = r.getString("TargetObj");
		description = r.getString("Description");
		criteriaType = r.getString("CriteriaType");
		criteriaRex = r.getString("CriteriaRex");

		//criteriaRexが""に囲まれていたら、外す。
		Pattern t_pat0 = Pattern.compile("^\\s*\"([\\s\\S]+)\"\\s*$");
		Matcher t_m = t_pat0.matcher(criteriaRex);
		if (t_m.find()) {
			criteriaRex = t_m.group(1);
		}

		id = r.getInt("ID");
	}

	protected static Date start;
	protected static Date end;
	public static void setStart(Date d) {
		start = d;
		end = null;//リセットする。setEndで初期化状態かどうかの判定に使用。
	}

	public static void setEnd(Date d) {
		if (end == null) {
			//一度セットされたら上書きしない。
			end = d;
		}
	}

	//ミリ秒を返す
	public long getMiliSec() {
		return (end.getTime() - start.getTime());
	}

	//includeされたTestCaseの名前が割り当てられる。IDの修飾子である。
	public String m_prefix = "";

	//仮想メンバ変数でロストしたメンバ変数名。Inspectから記入される。
	private static String m_lost;
	public static void setLost(String cname, String mname) {
		if (cname == null) {
			m_lost = null;
		} else {
			m_lost = cname + "#" + mname;
		}
	}

	//その他、ロストしそうなメッセージを書く。
	public static void setLost(String str) {
		m_lost = str;
	}

	public AssertRecord _dup(boolean notNew) {
		AssertRecord t_rec = new AssertRecord(2);
		t_rec.id = id;
		t_rec.targetObj = targetObj;
		t_rec.criteriaType = criteriaType;
		t_rec.criteriaRex = criteriaRex;
		t_rec.description = description;

		if (notNew) {
			t_rec.m_status = m_status;
		}
		return t_rec;
	}

	//登録時のエスケープ処理用
	private String _targetObj() {
		return esq(targetObj);
	}

	//登録時のエスケープ処理用
	private String _criteriaRex() {
		return esq(criteriaRex);
	}

	public String getUpdateSQL() {
		if (removed && isNew()) {
			return null;//DBに未登録のまま削除
		}

		if (targetObj == null && criteriaRex == null) return null;

		if (removed) {
			return "delete from \"tbl_AssertRecord\" where \"ID\"='" + Integer.toString(id) + "'";
		}

		if (isNew()) {
			return "insert into \"tbl_AssertRecord\" (\"ID\", \"TargetObj\", \"CriteriaType\", \"CriteriaRex\", \"Description\", \"Completed\") "
					+ "values ('" + Integer.toString(id) + "','" + _targetObj() + "','" + criteriaType + "','" + _criteriaRex() + "','"
					+ description + "','" + Boolean.toString(isCompleted()) + "')" ;
		} else if (isUpdated()) {
			return "update \"tbl_AssertRecord\" set \"TargetObj\"='" + _targetObj() + "', \"CriteriaType\"='" + criteriaType
					+ "', \"CriteriaRex\"='" + _criteriaRex()	+ "', \"Description\"='" + description
					+ "', \"Completed\"='" + Boolean.toString(isCompleted())
					+ "' where \"ID\"=" + Integer.toString(id);
		}
		return null;
	}

	//Assertのレコードがすべて入力されたかどうか。
	public boolean isCompleted() {
		if (criteriaType != null && criteriaRex != null) {
			if ("Exception".equals(criteriaType) && "$null".equals(criteriaRex)) {
				//VarDictionar, CodeFairyの場合、ExceptionでなければOK。
				return true;
			}

			if (id > 0 && targetObj != null) {
				if (targetObj.length() > 0 && criteriaType.length() > 0 && criteriaRex.length() > 0) {
					//先頭が「!」は未完成の印。
					if (targetObj.startsWith("!")) {
						return false;
					}
					//正規表現が不正なら未完成。
					try {
						String[] rexlist =  (String[]) Fson.fromJson(criteriaRex, String[].class);

						if (rexlist == null) {
							return false;
						}
						for (int i = 0; i < rexlist.length; i++) {
							String t_rex = rexlist[i];
							if (t_rex != null) {
								@SuppressWarnings("unused")
								Pattern t_pat = Pattern.compile(t_rex);
							}
						}

						//正規表現のコンパイルが全て成功している。
						return true;

					} catch (Exception e) {
						return false;
					}
				}
			}
		}

		return false;
	}

	//ダミー
	public int hashCode() {
		assert false;
		return 0;
	}

	//Assert同士の同値判定t_
	public boolean equals(Object ast) {
		if (ast == null) {
			return false;
		}
		AssertRecord t_ast = (AssertRecord)ast;

		boolean result = true;
		if (targetObj != null) {
			if (!targetObj.equals(t_ast.targetObj)) {
				result = false;
			}
		} else if (t_ast.targetObj != null) {
			result = false;
		}

		if (criteriaRex != null) {
			if (!criteriaRex.equals(t_ast.criteriaRex)) {
				result = false;
			}
		} else if (t_ast.criteriaRex != null) {
			result = false;
		}

		if (criteriaType != null) {
			if (!criteriaType.equals(t_ast.criteriaType)) {
				result = false;
			}
		} else if (t_ast.criteriaType != null) {
			result = false;
		}
		return result;
	}

	static final Pattern remove_space = Pattern.compile("\\A\\s*([^\\s].*[^\\s])\\s*\\Z", Pattern.DOTALL);

	//実行結果の対象オブジェクトの文字列表現を引数にして、ログをTool経由で出力する。
	@SuppressWarnings("unchecked")
	public void evaluate(int phase, TestCommandRecord rec) {
		long msec = getMiliSec();

		ArrayList<Object> target = null;
		try {
			target = getResultString(rec);
		} catch (SecurityException | IllegalArgumentException
				| InstantiationException | IllegalAccessException
				| ClassNotFoundException | NoSuchMethodException
				| InvocationTargetException | NoSuchFieldException
				| CannotCompileException | NoSuchElementException | ParseException e) {
			//ほとんどほ原因はコンパイルとsetbodyなので、ソースも出力する。
			TestLogger.cmdCauseError(msec, Tool.getStackMessage(e, 0, 10) + "\\n source = " + targetObj, m_prefix, e);
			return;
		} catch (Error e) {
			TestLogger.cmdCauseError(msec, Tool.getStackMessage(e, 0, 10), m_prefix, e);
			return;
		} catch (Exception e) {
			TestLogger.cmdCauseError(msec, Tool.getStackMessage(e, 0, 10), m_prefix, e);
			return;
		}
		String t_message = "";
		boolean b_result = true;
		ArrayList<Object> p_str = new ArrayList<Object>();

		//Exceptionの場合、空白のまま放置することもある。
		if (criteriaRex == null) {
			//判定対象外
			return;
		} else {
			//正規表現の否定形かどうか判定する。否定形なら、判定は逆転する。
			p_str = (ArrayList<Object>) Tool.getObjectfromJSON(ArrayList.class, criteriaRex);//判定条件の配列（デシジョンテーブルを考慮）

			if (target == null || target.size() != p_str.size()) {
				//数が合わない。
				TestLogger.cmdFailed(msec, "Asserting Objects must be " + Integer.toString(p_str.size()) + " elements.", m_prefix, p_str.size());
				return;
			}

			for (int i = 0; i < p_str.size(); i++) {
				Object crexobj = p_str.get(i);

				if (target.get(i) == null ) {
					//何もできない。後でフォロー
					if (!isEquivalentTo(target.get(i), crexobj.toString())) {
						b_result = false;
					}
				} else 	if (target.get(i).getClass().equals(String.class)) {
					//getResultString()から得たtargetの要素が文字列であれば以下の処理をする。

					String crex = crexobj.toString();
					if (crex == null) {
						//正規表現が指定されていない場合は判定しない。
						//※入力値により、フォーカスして評価する引数や返却値を限定したい場合がある。
						//　そのような場合、評価対象外の意味を持たせるために空の要素を与えればよい。
					} else if (crex.toLowerCase().startsWith("select")) {
						//DBアクセスで判定する場合、target.get(i)がtrueなら成功。
						if (!target.get(i).toString().equals("true")) {
							b_result = false;
						}
					} else if (crex.startsWith("verify(")) {
						//目視で確認
						String t_msg = crex.replaceFirst("^verify\\((.*)\\)$", "$1");
						t_msg = "Verify! >> " + target.get(i).toString() + "\n" + t_msg;
						JOptionPane optionPane = new JOptionPane(t_msg, JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_NO_OPTION, null, new Object[]{"YES", "NO"}, "YES");
						JDialog t_dialog = optionPane.createDialog(null, "Verification");
						t_dialog.setLocation(0, 0);
						t_dialog.setVisible(true);
						Object res = optionPane.getValue();

						if(res.toString().equals("NO")) {
							//確認の結果、失敗。
							b_result = false;
						}
					} else if (crex.startsWith("$")) {
						//最初に不等号のための文字列比較を行う。
						boolean b_big = false;
						boolean b_eq = false;
						if (crex.startsWith("$B")) {  //big型かどうか。
							b_big = true;
						}
						if (crex.matches("^\\$B?[><]?=.*$")) {  //等号が付くかどうか。
							b_eq = true;
						}
						String t_num = crex.replaceFirst("^[\\$<>=B]*\\s*([^\\s]+)\\s*$", "$1");
						String result_num = target.get(i).toString();

						if (crex.matches("^\\$B?>\\s*[0-9\\.\\-\\+]+\\s*$")) {
							if (!VCentral.greaterThan(result_num, t_num, b_eq, b_big)) {
								b_result = false;
							}
						} else if (crex.matches("^\\$B?<\\s*[0-9\\.\\-\\+]+\\s*$")) {
							if (!VCentral.lessThan(result_num, t_num, b_eq, b_big)) {
								b_result = false;
							}
						} else if (b_eq) {
							//演算子は$=から始まる。等しい.null比較も考慮する。
							if (t_num.matches("^[0-9\\.\\-\\+]+$")) {
								if (!VCentral.equalN(result_num, t_num, b_big)) {
									b_result = false;
								}
							} else if (crex.matches("^\\$=\\s*null\\s*$")) {
								//$=null。=でnull比較の場合、
								if (!result_num.equals("$null")) {
									//期待した結果（null）と一致しない。
									b_result = false;
								}
							} else if (crex.indexOf("$=~")==0) {
								//正規表現で比べる。
								Pattern xpat = Pattern.compile(crex.replaceFirst("^\\$=\\~\\s*([^\\s]+)\\s*$", "$1"));
								Matcher xmat = xpat.matcher(result_num);
								b_result = xmat.find();
							} else {
								//単なる文字列比較
								b_result = t_num.replace("$=","").trim().equals(result_num);
							}
						}
					} else {
						//正規表現の本体の両側に空白や改行があれば削除して成型する。
						Matcher t_m = remove_space.matcher(crex);
						if (t_m.find()) {
							crex = t_m.group(1);
						}
						//crexが空の場合、nullを待っているとみなす。
						if (crex.length() == 0 || crex.equals("$null")) {
							crex = "\\$null";
						}

						Pattern t_pat = Pattern.compile(crex);
						if (criteriaType.equals("Exception")) {
							//ここに入ってくる時点でExceptionは検出されていない。
							//Exceptionを前提とする場合、decisionは1つである。
							if (crex.equals("\\$null")) {
								//Exceptionがnullと期待されている場合。
								TestLogger.cmdIsOK(msec, m_prefix, p_str.size());
								return;
							}
						} else if (criteriaType.equals("Inspect")) {
							String tobe_compared = target.get(i).toString();
							t_m = t_pat.matcher(tobe_compared);
							if (!t_m.find()) {
								b_result = false;
							}
						}
					}
				} else if (crexobj != null) {
					//文字列でない場合、JSON化してお互いを比べる。
					if (!isEquivalentTo(target.get(i), crexobj.toString())) {
						b_result = false;
					}
				}
				if (crexobj == null) {
					//正規表現が指定されていないので判定しない。
					t_message += Integer.toString(i + 1) + "th=SKIPPED (EXPECTED)]\n";
				} else if (b_result) {
					//有効を成功とみなす。もしくは失敗を成功とみなす。
					t_message += Integer.toString(i + 1) + "th=TRUE (EXPECTED)]\n";
				} else	if (target.get(i) == null ) {
					//ここでフォロー:getResultStringが失敗
					t_message += Integer.toString(i + 1) + "th=FALSE (EVAL　FAILED):value=null in regex=" + crexobj.toString() + "\n";
				} else {
					String j_target = Tool.getJSONfromObject(target.get(i));
					t_message += Integer.toString(i + 1) + "th=FALSE (UNEXPECTED):value=" + j_target + " in regex=" + crexobj.toString() + "\n";
				}
			}
		}
		if (b_result) {
			//意図した成功もしくは意図した失敗判定の場合OK
			TestLogger.cmdIsOK(msec, m_prefix, p_str.size());
		} else {
			TestLogger.cmdFailed(msec, t_message, m_prefix, p_str.size());
		}
	}

	//結果としてのオブジェクトが、Jsonで記述されたポイントを押さえているかで、等価であるかどうか判定する。
	private boolean isEquivalentTo(Object obj, String json) {
		if (json == null) {
			if (obj == null) {
				return true;
			}
			return false;
		} else if (obj == null) {
			return false;
		}
		Class<?> cl = obj.getClass();
		if ((Tool.isPrimitive2(cl) && !cl.isArray()) || cl.isEnum()) {
			//基本オブジェクトもしくは文字列の完全一致。※enumは定義順の数字で比較する。
			return obj.equals(Tool.getObjectfromJSON(cl, json));
		}

		//map,list,配列については各要素を復元して比較する。
		if (java.util.Map.class.isAssignableFrom(cl) && json.matches("^\\{[\\s\\S]+\\}$")) {
			//jsonはマップ型. 判定のポイントを絞っている可能性があるので、サイズは比べない。
			@SuppressWarnings("unchecked")
			HashMap<Object,Object> t_map = (HashMap<Object,Object>)Tool.getObjectfromJSON(HashMap.class, json);
			@SuppressWarnings("unchecked")
			HashMap<Object,Object> t_obj = (HashMap<Object,Object>)obj;

			if (t_map != null) {
				for (Entry<Object, Object> entry :t_map.entrySet()) {
					if (t_obj.containsKey(entry.getKey())) {
						if (!t_obj.get(entry.getKey()).equals(entry.getValue())) {
							return false;
						}
					} else {
						return false;
					}
				}
				return true;
			}
			return false;
		}
		if ((cl.isArray() || java.util.List.class.isAssignableFrom(cl)) && json.matches("^\\[[\\s\\S]+\\]$")) {
			//jsonはList型。どちらもListにして比較。
			@SuppressWarnings("unchecked")
			ArrayList<Object> t_list1 = (ArrayList<Object>)Tool.getObjectfromJSON(ArrayList.class, json);
			@SuppressWarnings("unchecked")
			ArrayList<Object> t_list2 = (ArrayList<Object>)Tool.getObjectfromJSON(ArrayList.class, Tool.getJSONfromObject(obj));
			if (t_list1 != null && t_list2 != null && t_list1.size() == t_list2.size()) {
				//配列は、サイズが等しいことが前提。
				for (int i = 0; i < t_list1.size(); i++) {
					if (t_list1.get(i) == null && t_list2.get(i) == null) {
						//双方のオブジェクトが等価である。
					} else if (t_list1.get(i).equals(t_list2.get(i))) {
						//双方のオブジェクトが等価である。
					} else {
						return false;
					}
				}
				return true;
			}
			return false;
		}

		//Object比較
		@SuppressWarnings("unchecked")
		HashMap<String,Object> t_map = (HashMap<String,Object>)Tool.getObjectfromJSON(HashMap.class, json);
		if (t_map != null) {
			for (Entry<String, Object> entry :t_map.entrySet()) {
				ArrayList<Object> t_list = Fson.valueByJsonPath(obj, "$." + entry.getKey());
				if (t_list.size() == 1) {
					//判定のポイントとなる属性が等しければ等価とみなす。
					if (!isEquivalentTo(t_list.get(0), Tool.getJSONfromObject(entry.getValue()))) {
						return false;
					}
				} else {
					return false;
				}
			}
			return true;
		}
		return false;
	}


	//メソッド実行時にExceptionが検出された場合。
	//exceptsが不定長なのは、内部のテスト実行と外部プロセス実行の二つから例外が発生する可能性があるため。
	public void evaluateException(int phase, TestCommandRecord rec, String fakeInfo, Throwable... excepts) {
		long msec = getMiliSec();
		if (excepts != null) {
			String internal_msg = "";
			String t_stacks = "";
			for (int i = 0; i < excepts.length; i++) {
				if (excepts[i].getMessage() == null || excepts[i].getMessage().length() == 0) {
					String str_cause = "Inspector.\n";
					if (phase < 200 && phase >= 100) {
						switch (phase) {
							case 150:str_cause = "Cannot find the method.\n";break;
							case 152:str_cause = "Exception in the invokation.\n";break;
							case 100:str_cause = "Can not find the class to construct.\n";break;
							case 101:str_cause = "Failed to make snapshot.\n";break;
							case 120:str_cause = "Failed to invoke the constructor.\n";break;
							default:str_cause = "invokation.\n";
						}
						if (fakeInfo != null) {
							str_cause += fakeInfo + "\n";
						}
					} else if (phase < 100) {
						str_cause = "constructing arguments?.\n";
					}

					internal_msg = ": occurred in the " + str_cause;;
				}
				internal_msg += excepts[i].getClass().getName() + " caused by \n" + Tool.removeRedundancy(excepts[i], "") + ".\n";
				t_stacks += Tool.getStackMessage(excepts[i], 0, 10);
			}


			//仮想メンバ変数が見つからないため、null参照を起こした場合。
			if (m_lost != null) {
				internal_msg = m_lost + " not found. " + internal_msg;
			}

			if (criteriaType.equals("Exception")) {
				if (excepts.getClass().getName().matches(Pattern.quote(criteriaRex))) {
					//予想通りのException
					TestLogger.cmdIsOK(msec, m_prefix, 1);
				} else {
					//予想外のException
					TestLogger.cmdCauseError(msec, internal_msg + t_stacks, m_prefix, excepts);
				}
			} else {
				//予想外のException
				TestLogger.cmdCauseError( msec, internal_msg + t_stacks, m_prefix, excepts);
			}
		}
	}

	//テストで焦点を当てたオブジェクトの状態を取得する。
	protected ArrayList<Object> getResultString(TestCommandRecord curCmd) throws SecurityException,
				IllegalArgumentException, CannotCompileException, InstantiationException, IllegalAccessException, ClassNotFoundException,
				NotFoundException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException, ParseException, NotSupportedClassException {
		//文字列の中でインスタンス（例：1234$_）を示すことがあるのでエスケープ。
		ArrayList<String> t_esqlist = CodeProcessor.escDQStrings(targetObj, "@DQT");

		String t_targetObj = t_esqlist.get(0);
		String t_src = t_targetObj;

		//$nの記述を、実際にアクセスできるように新変数に置き換える。(冒頭でgetInstance()で取得するようにする)
		HashMap<String, String> cmap = new HashMap<String, String>();

		//使っている$変数のリスト
		ArrayList<String> t_list = new ArrayList<String>();

		//srcに$変数の直接参照や演算が含まれている場合にマップに登録し、新変数に置き換える。なければ、判定不可能なのでnull。
		//Assertは実行オブジェクト外で行われるため、$.*はそのままでは使えないからである。
		Pattern t_pat2 = Pattern.compile("([\\(=\\;\\s\\+\\-\\*/,\\[\\|\\&])\\$([_0-9]+)([\\.;/=\\+\\*\\s\\-\\)\\[\\|\\&\\]])"); //$NON-NLS-1$
		Matcher t_m = t_pat2.matcher(targetObj);
		while (t_m.find()) {
			String w_str = t_m.group();
			t_src = t_src.replaceFirst(Pattern.quote(w_str), Matcher.quoteReplacement(t_m.group(1) + "_x" + t_m.group(2) + t_m.group(3))); //$NON-NLS-1$
			String t_curvar = t_m.group(2);
			if (!t_list.contains("$" + t_curvar)) { //$NON-NLS-1$
				t_list.add("$" + t_curvar); //$NON-NLS-1$

				String t_key = "_x" + t_curvar;//引数のキー
				if (!cmap.containsKey(t_key)) {
					//前段に宣言を入れるためにクラス情報を格納する。
					cmap.put(t_key, curCmd.getArgClassByName("$" + t_curvar));
				}
			}
		}

		t_src = CodeProcessor.restoreDQString(t_src, "@DQT", t_esqlist);

		//宣言を先頭に追加
		for (int i = 0; i < t_list.size(); i++) {
			String t_str = t_list.get(i).substring(1);

			//_xを引数の$の代わりにする。$のままだとInspectorの引数と解釈されるため。
			String v_str = "_x" + t_str; //$NON-NLS-1$
			String dec_str = cmap.get(v_str) + " " + v_str + "=(" + cmap.get(v_str) + ")getInstance(\"" + Integer.toString(id) + "$" + t_str + "\");"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
			t_src = dec_str + t_src;
		}

		//privateフィールドアクセスできるようにソースを展開する。
		StringBuilder t_b = new StringBuilder(t_src);
		HashMap<String, Class<?>> t_map = new HashMap<String, Class<?>>();
		for(Entry<String, String> entry : cmap.entrySet()) {
			Class<?> t_c = Tool.forName(entry.getValue());
			t_map.put(entry.getKey(), t_c);
		}
		CodeProcessor.analyzeVarToClass(t_b, t_map);
		//この時点でソースは{}に囲まれている。
		t_src = t_b.toString();

		int t_id = id;
		if (snapshot_id > 0) {
			t_id = snapshot_id;
		}

		return doInspect(t_id, t_src);
	}

	//指定のソースを実行しオブジェクトの内部情報を取得する。
	private ArrayList<Object> doInspect(int id, String src) throws SecurityException, IllegalArgumentException, CannotCompileException, InstantiationException, IllegalAccessException, ClassNotFoundException, NotFoundException, NoSuchMethodException, InvocationTargetException {
		Inspector t_i = Tool.createInspector(id, src);
		if (t_i != null) {
			t_i.inspect();

			return t_i.results;
		}
		return null;
	}

}
