module main

import os
import os.cmdline
import testing
import v.pref

struct Context {
mut:
	verbose   bool
	fail_fast bool
	run_only  []string
}

fn main() {
	args := os.args.clone()
	if os.args.last() == 'test' {
		show_usage()
		return
	}
	args_to_executable := args[1..]
	mut args_before := cmdline.options_before(args_to_executable, ['test'])
	mut args_after := cmdline.options_after(args_to_executable, ['test'])
	mut ctx := Context{}
	ctx.fail_fast = extract_flag_bool('-fail-fast', mut args_after, testing.fail_fast)
	ctx.verbose = extract_flag_bool('-v', mut args_after, false)
	ctx.run_only = extract_flag_string_array('-run-only', mut args_after, testing.test_only_fn)
	os.setenv('VTEST_ONLY_FN', ctx.run_only.join(','), true)
	if args_after == ['v'] {
		eprintln('`v test v` has been deprecated.')
		eprintln('Use `v test-all` instead.')
		exit(1)
	}
	backend_pos := args_before.index('-b')
	backend := if backend_pos == -1 { '.c' } else { args_before[backend_pos + 1] } // this giant mess because closures are not implemented

	mut ts := testing.new_test_session(args_before.join(' '), true)
	ts.fail_fast = ctx.fail_fast
	for targ in args_after {
		if os.is_dir(targ) {
			// Fetch all tests from the directory
			files, skip_files := ctx.should_test_dir(targ.trim_right(os.path_separator),
				backend)
			ts.files << files
			ts.skip_files << skip_files
			continue
		} else if os.exists(targ) {
			match ctx.should_test(targ, backend) {
				.test {
					ts.files << targ
					continue
				}
				.skip {
					if ctx.run_only.len > 0 {
						continue
					}
					ts.files << targ
					ts.skip_files << targ
					continue
				}
				.ignore {}
			}
		} else {
			eprintln('\nUnrecognized test file `${targ}`.\n `v test` can only be used with folders and/or _test.v files.\n')
			show_usage()
			exit(1)
		}
	}
	ts.session_start('Testing...')
	ts.test()
	ts.session_stop('all V _test.v files')
	if ts.failed_cmds.len > 0 {
		exit(1)
	}
}

fn show_usage() {
	println('Usage:')
	println('   A)')
	println('      v test folder/ : run all v tests in the given folder.')
	println('      v -stats test folder/ : the same, but print more stats.')
	println('   B)')
	println('      v test file_test.v : run test functions in a given test file.')
	println('      v -stats test file_test.v : as above, but with more stats.')
	println('   Note: you can also give many and mixed folder/ file_test.v arguments after `v test` .')
	println('')
}

pub fn (mut ctx Context) should_test_dir(path string, backend string) ([]string, []string) { // return is (files, skip_files)
	mut files := os.ls(path) or { return []string{}, []string{} }
	mut local_path_separator := os.path_separator
	if path.ends_with(os.path_separator) {
		local_path_separator = ''
	}
	mut res_files := []string{}
	mut skip_files := []string{}
	for file in files {
		p := path + local_path_separator + file
		if os.is_dir(p) && !os.is_link(p) {
			if file == 'testdata' {
				continue
			}
			ret_files, ret_skip_files := ctx.should_test_dir(p, backend)
			res_files << ret_files
			skip_files << ret_skip_files
		} else if os.exists(p) {
			match ctx.should_test(p, backend) {
				.test {
					res_files << p
				}
				.skip {
					if ctx.run_only.len > 0 {
						continue
					}
					res_files << p
					skip_files << p
				}
				.ignore {}
			}
		}
	}
	return res_files, skip_files
}

enum ShouldTestStatus {
	test // do test, print OK or FAIL, depending on if it passes
	skip // print SKIP for the test
	ignore // just ignore the file, so it will not be printed at all in the list of tests
}

fn (mut ctx Context) should_test(path string, backend string) ShouldTestStatus {
	if path.ends_with('mysql_orm_test.v') {
		testing.find_started_process('mysqld') or { return .skip }
	}
	if path.ends_with('pg_orm_test.v') {
		testing.find_started_process('postgres') or { return .skip }
	}
	if path.ends_with('onecontext_test.v') {
		return .skip
	}
	$if tinyc {
		if path.ends_with('naked_attr_test.amd64.v') {
			return .skip
		}
	}
	if path.ends_with('_test.v') {
		return ctx.should_test_when_it_contains_matching_fns(path, backend)
	}
	if path.ends_with('_test.js.v') {
		if testing.is_node_present {
			return ctx.should_test_when_it_contains_matching_fns(path, backend)
		}
		return .skip
	}
	if path.ends_with('.v') && path.count('.') == 2 {
		if !path.all_before_last('.v').all_before_last('.').ends_with('_test') {
			return .ignore
		}
		backend_arg := path.all_before_last('.v').all_after_last('.')
		arch := pref.arch_from_string(backend_arg) or { pref.Arch._auto }
		if arch == pref.get_host_arch() {
			return ctx.should_test_when_it_contains_matching_fns(path, backend)
		} else if arch == ._auto {
			if backend_arg == 'c' { // .c.v
				return if backend == 'c' {
					ctx.should_test_when_it_contains_matching_fns(path, backend)
				} else {
					ShouldTestStatus.skip
				}
			}
			if backend_arg == 'js' {
				return if backend == 'js' {
					ctx.should_test_when_it_contains_matching_fns(path, backend)
				} else {
					ShouldTestStatus.skip
				}
			}
		} else {
			return .skip
		}
	}
	return .ignore
}

fn (mut ctx Context) should_test_when_it_contains_matching_fns(path string, backend string) ShouldTestStatus {
	if ctx.run_only.len == 0 {
		// no filters set, so just compile and test
		return .test
	}
	lines := os.read_lines(path) or { return .ignore }
	for line in lines {
		if line.match_glob('fn test_*') || line.match_glob('pub fn test_*') {
			tname := line.replace_each(['pub fn ', '', 'fn ', '']).all_before('(')
			for pattern in ctx.run_only {
				mut pat := pattern.clone()
				if pat.contains('.') {
					pat = pat.all_after_last('.')
				}
				if tname.match_glob(pat) {
					if ctx.verbose {
						println('> compiling path: ${path}, since test fn `${tname}` matches glob pattern `${pat}`')
					}
					return .test
				}
			}
		}
	}
	return .ignore
}

fn extract_flag_bool(flag_name string, mut after []string, flag_default bool) bool {
	mut res := flag_default
	orig_after := after.clone() // workaround for after.filter() codegen bug, when `mut after []string`
	matches_after := orig_after.filter(it != flag_name)
	if matches_after.len < after.len {
		after = matches_after.clone()
		res = true
	}
	return res
}

fn extract_flag_string_array(flag_name string, mut after []string, flag_default []string) []string {
	mut res := flag_default.clone()
	mut found := after.index(flag_name)
	if found > -1 {
		if found + 1 < after.len {
			res = after[found + 1].split_any(',')
			after.delete(found)
		}
		after.delete(found)
	}
	return res
}
