// vtest flaky: true
// vtest retry: 3
import orm
import db.sqlite

struct MockDBState {
mut:
	last  string
	data  []orm.Primitive
	where []orm.Primitive
}

struct MockDB {
	use_num bool
	st      &MockDBState = unsafe { nil }
	db      sqlite.DB
}

fn MockDB.new() &MockDB {
	return &MockDB{
		st: &MockDBState{}
		db: sqlite.connect(':memory:') or { panic(err) }
	}
}

fn (db MockDB) @select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive {
	mut st := db.st
	st.last = orm.orm_select_gen(config, '`', false, '?', 5, where)
	st.data = data.data
	st.where = where.data
	return db.db.@select(config, data, where)
}

fn (db MockDB) insert(table string, data orm.QueryData) ! {
	mut st := db.st
	last, qdata := orm.orm_stmt_gen(.sqlite, table, '`', .insert, false, '?', 1, data,
		orm.QueryData{})
	st.last = last
	st.data = qdata.data
	st.where = []orm.Primitive{}
	return db.db.insert(table, data)
}

fn (db MockDB) update(table string, data orm.QueryData, where orm.QueryData) ! {
	mut st := db.st
	st.last, _ = orm.orm_stmt_gen(.sqlite, table, '`', .update, false, '?', 1, data, where)
	st.data = data.data
	st.where = where.data
	return db.db.update(table, data, where)
}

fn (db MockDB) delete(table string, where orm.QueryData) ! {
	mut st := db.st
	st.last, _ = orm.orm_stmt_gen(.sqlite, table, '`', .delete, false, '?', 1, orm.QueryData{},
		where)
	return db.db.delete(table, where)
}

const typ_to_typename = {
	typeof[i8]().idx:     'i8'
	typeof[i16]().idx:    'i16'
	typeof[int]().idx:    'int'
	typeof[i64]().idx:    'i64'
	typeof[u8]().idx:     'u8'
	typeof[u16]().idx:    'u16'
	typeof[u32]().idx:    'u32'
	typeof[u64]().idx:    'u64'
	typeof[f32]().idx:    'f32'
	typeof[f64]().idx:    'f64'
	typeof[string]().idx: 'string'
	typeof[bool]().idx:   'bool'
	orm.serial:           'serial'
	orm.time_:            'time'
	orm.enum_:            'enum'
}

fn mock_type_from_v(typ int) !string {
	return if typ in typ_to_typename {
		'${typ_to_typename[typ]}-type'
	} else {
		error('unknown type ${typ}')
	}
}

fn (db MockDB) create(table string, fields []orm.TableField) ! {
	mut st := db.st
	st.last = orm.orm_table_gen(table, '`', true, 0, fields, mock_type_from_v, false)!
	return db.db.create(table, fields)
}

fn (db MockDB) drop(table string) ! {
	return db.db.drop(table)
}

fn (db MockDB) last_id() int {
	return db.db.last_id()
}

// --

@[table: 'foo']
struct Foo {
mut:
	id u64    @[primary; sql: serial]
	a  string
	//	b  string  [default: '"yes"']
	c ?string
	d ?string = 'hi'
	e int
	//	f  int     [default: 33]
	g ?int
	h ?int = 55
}

fn test_option_struct_fields_and_none() {
	db := MockDB.new()

	sql db {
		create table Foo
	}!
	assert db.st.last == 'CREATE TABLE IF NOT EXISTS `foo` (`id` serial-type NOT NULL, `a` string-type NOT NULL, `c` string-type, `d` string-type, `e` int-type NOT NULL, `g` int-type, `h` int-type, PRIMARY KEY(`id`));'

	_ := sql db {
		select from Foo where e > 5 && c is none && c !is none && h == 2
	}!
	assert db.st.last == 'SELECT `id`, `a`, `c`, `d`, `e`, `g`, `h` FROM `foo` WHERE `e` > ? AND `c` IS NULL AND `c` IS NOT NULL AND `h` = ?;'
	assert db.st.data.len == 0
	assert db.st.where.len == 2
	assert db.st.where == [orm.Primitive(int(5)), orm.Primitive(int(2))]

	foo := Foo{}
	sql db {
		insert foo into Foo
	}!
	assert db.st.last == 'INSERT INTO `foo` (`a`, `c`, `d`, `e`, `g`, `h`) VALUES (?, ?, ?, ?, ?, ?);'
	assert db.st.data.len == 6
	assert db.st.data == [orm.Primitive(string('')), orm.Null{}, orm.Primitive(string('hi')), int(0),
		orm.Null{}, int(55)]
	id := db.last_id()

	res1 := sql db {
		select from Foo where id == id
	}!
	assert db.st.last == 'SELECT `id`, `a`, `c`, `d`, `e`, `g`, `h` FROM `foo` WHERE `id` = ?;'
	assert db.st.data.len == 0
	assert db.st.where.len == 1
	assert db.st.where == [orm.Primitive(int(id))]
	assert res1.len == 1
	assert res1[0] == Foo{
		id: 1
		a: ''
		c: none
		d: 'hi'
		e: 0
		g: none
		h: 55
	}

	sql db {
		update Foo set c = 'yo', d = none, g = 44, h = none where id == id
	}!
	assert db.st.last == 'UPDATE `foo` SET `c` = ?, `d` = ?, `g` = ?, `h` = ? WHERE `id` = ?;'
	assert db.st.data.len == 4
	assert db.st.data == [orm.Primitive(string('yo')), orm.Null{}, int(44), orm.Null{}]
	assert db.st.where.len == 1
	assert db.st.where == [orm.Primitive(int(id))]

	res2 := sql db {
		select from Foo where id == id
	}!
	assert db.st.last == 'SELECT `id`, `a`, `c`, `d`, `e`, `g`, `h` FROM `foo` WHERE `id` = ?;'
	assert db.st.data.len == 0
	assert db.st.where.len == 1
	assert db.st.where == [orm.Primitive(int(id))]
	assert res2.len == 1
	assert res2[0] == Foo{
		id: 1
		a: ''
		c: 'yo'
		d: none
		e: 0
		g: 44
		h: none
	}

	assert sql db {
		select count from Foo where a == 'yo'
	}! == 0
	assert sql db {
		select count from Foo where d == 'yo'
	}! == 0
	assert sql db {
		select count from Foo where c == 'yo'
	}! == 1
	assert sql db {
		select count from Foo where a == ''
	}! == 1
	assert sql db {
		select count from Foo where d == ''
	}! == 0
	assert sql db {
		select count from Foo where c == ''
	}! == 0
	assert sql db {
		select count from Foo where a is none
	}! == 0
	assert sql db {
		select count from Foo where d is none
	}! == 1
	assert sql db {
		select count from Foo where c is none
	}! == 0
	assert sql db {
		select count from Foo where a !is none
	}! == 1
	assert sql db {
		select count from Foo where d !is none
	}! == 0
	assert sql db {
		select count from Foo where c !is none
	}! == 1
}

struct Bar {
	id   u64     @[primary; sql: serial]
	name ?string
	age  int
}

fn update_bar1(db MockDB, id u64, name ?string) ! {
	foo := 66
	sql db {
		update Bar set name = name, age = age + 3 + foo where id == id
	}!
}

fn update_bar2(db MockDB, name ?string, new_name ?string) ! {
	sql db {
		update Bar set name = new_name where name == name
	}!
}

type NameFn = fn () ?string

fn update_bar3(db MockDB, name_fn NameFn, new_name string) ! {
	sql db {
		update Bar set name = new_name where name == name_fn()
	}!
}

fn test_inserting_passed_optionals() {
	db := MockDB.new()

	entry1 := Bar{}
	entry2 := Bar{
		name: 'Alice'
		age: 55
	}
	entry3 := Bar{
		name: 'Bob'
		age: 66
	}
	sql db {
		create table Bar
		insert entry1 into Bar
		insert entry2 into Bar
		insert entry3 into Bar
	}!

	update_bar1(db, 2, none)!
	update_bar1(db, 1, 'hi')!

	res1 := sql db {
		select from Bar
	}!
	assert res1.len == 3
	assert res1[0].name or { '' } == 'hi'
	assert res1[1].name == none
	assert res1[2].name or { '' } == 'Bob'

	update_bar2(db, none, 'xxx')! // no effect (select using "is none", not "== none")
	update_bar2(db, 'hi', none)!

	res2 := sql db {
		select from Bar
	}!
	assert res2.len == 3
	assert res2[0].name == none
	assert res2[1].name == none
	assert res2[2].name or { '' } == 'Bob'

	update_bar3(db, fn () ?string {
		return none // no effect (select using "is none", not "== none")
	}, 'yyy')!
	update_bar3(db, fn () ?string {
		return 'Bob'
	}, 'www')!

	res3 := sql db {
		select from Bar
	}!
	assert res3.len == 3
	assert res3[0].name == none
	assert res3[1].name == none
	assert res3[2].name or { '' } == 'www'
}
