import { ApplicationState } from "../js/application_state.mjs"
import { RemoteState } from "../js/remote_state.mjs"
import { assert_equal } from "/assert.mjs"


class StateManagerMock {
	#next_application_state_change_resolve
	#next_application_state_change

	constructor() {
		this.state_changed()
	}

	state_changed() {
		const resolve = this.#next_application_state_change_resolve
		this.#next_application_state_change = new Promise((next_resolve) => this.#next_application_state_change_resolve = next_resolve)
		resolve?.({ promise: this.#next_application_state_change })
	}


	async *states() {
		let next_change = this.#next_application_state_change
		while (true) {
			yield
			next_change = (await next_change).promise
		}
	}
}

class HakubanExchangeMock {

	constructor() {
		this.object_observe_contracts = []
		this.tag_observe_contracts = []
	}

	object_observe_contract(tags, json) {
		return new HakubanObjectObserveContractBuilderMock(this, tags, json)
	}

	tag_observe_contract(tag) {
		return new HakubanTagObserveContractBuilderMock(this, tag)
	}
}

class HakubanObjectObserveContractBuilderMock {

	constructor(exchange, tags, json) {
		Object.assign(this, { exchange, tags, json })
	}

	build() {
		const contract = new HakubanObjectObserveContractMock(this.exchange, this.tags, this.json)
		this.exchange.object_observe_contracts.push(contract)
		return contract
	}
}

class HakubanTagObserveContractBuilderMock {

	constructor(exchange, tag) {
		Object.assign(this, { exchange, tag })
	}

	build() {
		const contract = new HakubanTagObserveContractMock(this.exchange, this.tag)
		this.exchange.tag_observe_contracts.push(contract)
		return contract
	}
}

class HakubanObjectObserveContractMock {
	constructor(exchange, tags, json) {
		Object.assign(this, { exchange, tags, json })
		this.dropped = false
		this.next_stream = new Promise((resolve) => this.next_stream_resolve = resolve)
		this.streams = {}
		this.stream_counter = 0
	}

	descriptor() {
		return { tags: this.tags, json: this.json }
	}

	drop() {
		this.exchange.object_observe_contracts.splice(this.exchange.object_observe_contracts.findIndex((ctr)=> ctr === this), 1)
		this.dropped = true
		for (const stream of Object.values(this.streams)) {
			stream.produce()
		}
	}

	emit() {
		this.stream_counter++
		const new_stream = new HakubanObjectStateStreamMock(this, this.stream_counter)
		this.streams[this.stream_counter] = new_stream
		this.next_stream_resolve(new_stream)
		this.next_stream = new Promise((resolve) => this.next_stream_resolve = resolve)
		return new_stream
	}

	async for_each(block) {
		while (!this.dropped) {
			const value = await this.next_stream
			if (!this.dropped) await block(value)
		}
	}
}

class HakubanTagObserveContractMock {
	constructor(exchange, tag) {
		Object.assign(this, { exchange, tag })
		this.dropped = false
		this.next_stream = new Promise((resolve) => this.next_stream_resolve = resolve)
		this.streams = {}
		this.stream_counter = 0
	}

	descriptor() {
		return { json: this.tag }
	}

	drop() {
		this.exchange.tag_observe_contracts.splice(this.exchange.tag_observe_contracts.findIndex((ctr)=> ctr === this), 1)
		this.dropped = true
		for (const stream of Object.values(this.streams)) {
			stream.produce()
		}
	}

	emit() {
		this.stream_counter++
		const new_stream = new HakubanObjectStateStreamMock(this, this.stream_counter)
		this.streams[this.stream_counter] = new_stream
		this.next_stream_resolve(new_stream)
		this.next_stream = new Promise((resolve) => this.next_stream_resolve = resolve)
		return new_stream
	}

	async for_each_concurrent(block) {
		while (!this.dropped) {
			const value = await this.next_stream
			if (!this.dropped) block(value)
		}
	}
}


class HakubanObjectStateStreamMock {
	constructor(contract, id) {
		this.contract = contract
		this.id = id
		this.value = null
		this.next_change = new Promise((resolve) => this.next_change_resolve = resolve)
		this.production_counter = 0
	}

	terminate() {
		this.dropped = true
		this.produce()
	}

	descriptor() {
		return { tags: [], json: { id: this.id } }
	}

	produce() {
		this.production_counter++
		this.value = new HakubanStateMock(this.production_counter)
		const previous_resolve = this.next_change_resolve
		this.next_change = new Promise((resolve) => this.next_change_resolve = resolve)
		previous_resolve({ promise: this.next_change })

	}

	async for_each(block) {
		let next_change = this.next_change
		while (!this.dropped) {
			if (!this.dropped && this.value != null) await block(this.value)
			next_change = (await next_change).promise
		}
	}
}

class HakubanStateMock {
	constructor(value) {
		this.value = value
	}

	json_deserialize() {
		return { data: this.value }
	}
}



export async function runTests() {

	console.group("RemoteState")

	const exchange = new HakubanExchangeMock
	const manager = RemoteState.manager = new StateManagerMock()
	const remote = new RemoteState(exchange)
	const state = new ApplicationState(null, [], null, remote, null, null)

	console.group("object")

	assert_equal(0, 1, Object.keys(state.remote).length, 0)

	state.observe((definer) => {
		definer.observe("a", ["b"], "c")
	})
	assert_equal(0, 1, exchange.object_observe_contracts.length, 1)
	assert_equal(0, 1, state.remote.a, undefined)
	let stream = exchange.object_observe_contracts[0].emit()
	stream.produce()
	for await (const _state of manager.states()) {
		if (state.remote.a != undefined) break
	}
	assert_equal(0, 1, state.remote.a, {data:1})
	stream.produce()
	for await (const _state of manager.states()) {
		if (state.remote.a.data == 2) break
	}
	assert_equal(0, 1, state.remote.a, {data:2})

	state.observe((definer) => {
		definer.observe("a", ["b"], "c")
		state.scope("c").observe((scoped_definer) =>
			scoped_definer.observe("x",['y'],'z')
		)
	})
	assert_equal(0, 1, exchange.object_observe_contracts.length, 2)
	assert_equal(0, 1, state.scope("c").remote.x, undefined)
	stream = exchange.object_observe_contracts[1].emit()
	stream.produce()
	for await (const _state of manager.states()) {
		if (state.scope("c").remote.x != undefined) break
	}
	assert_equal(0, 1, state.scope("c").remote.x, {data:1})
	assert_equal(0, 1, state.remote.a, {data:2})


	state.observe((definer) => {
		state.scope("c").observe((scoped_definer) =>
			scoped_definer.observe("x",['y'],'z')
		)
	})
	assert_equal(0, 1, exchange.object_observe_contracts.length, 1)
	assert_equal(0, 1, state.scope("c").remote.x, {data:1})
	stream.produce()
	for await (const _state of manager.states()) {
		if (state.remote.a == undefined) break
	}

	state.observe((_definer) => {
	})
	assert_equal(0, 1, exchange.object_observe_contracts.length, 0)
	stream.produce()
	for await (const _state of manager.states()) {
		if (state.scope("c").remote.x == undefined) break
	}

	console.groupEnd()


	console.group("wildcard")
	state.observe((definer) => {
		definer.observe("a", ["b"], (descriptor)=>descriptor.json.id)
	})
	assert_equal(0, 1, exchange.tag_observe_contracts.length, 1)
	assert_equal(0, 1, state.remote.a, {})
	stream = exchange.tag_observe_contracts[0].emit()
	stream.produce()
	for await (const _state of manager.states()) {
		if (Object.keys(state.remote.a).length != 0) break
	}
	assert_equal(0, 1, state.remote.a, { 1: { data: 1} })
	stream.produce()
	for await (const _state of manager.states()) {
		if (state.remote.a[1].data == 2) break
	}
	assert_equal(0, 1, state.remote.a[1], {data:2})

	stream.terminate()
	for await (const _state of manager.states()) {
		if (state.remote.a[1] == undefined) break
	}
	assert_equal(0, 1, state.remote.a, {})
	console.groupEnd()


	console.group("mixing objects with wildcards")
	state.observe((definer) => {
		definer.observe("a", ["b"], (descriptor)=>descriptor.json.id)
	})
	stream = exchange.tag_observe_contracts[0].emit()
	stream.produce()
	for await (const _state of manager.states()) {
		if (Object.keys(state.remote.a).length != 0) break
	}
	state.observe((definer) => {
		definer.observe("a", ["b"], "c")
	})
	assert_equal(0, 1, exchange.object_observe_contracts.length, 1)
	assert_equal(0, 1, state.remote.a, undefined)
	stream = exchange.object_observe_contracts[0].emit()
	stream.produce()
	for await (const _state of manager.states()) {
		if (state.remote.a != undefined) break
	}
	assert_equal(0, 1, state.remote.a, {data:1})

	console.groupEnd()

	console.groupEnd()

	document.getElementById("remote_result").innerHTML = "OK"
}
