import {describe, expect, it} from "vitest";
import {circular, defineModel, s} from "../src/library";

/**
 * Test class of an account.
 */
class Account
{
	static model = s.defineModel({
		Class: Account,
		identifier: "id",
		properties: {
			id: s.property.numeric(),
			createdAt: s.property.date(),
			name: s.property.string(),
			email: s.property.string(),
			active: s.property.boolean(),
		},
	});

	id: number;
	createdAt: Date;
	name: string;
	email: string;
	active: boolean;
}

/**
 * Test class of an article.
 */
class Article
{
	static model = s.defineModel({
		Class: Article,
		identifier: "id",
		properties: {
			id: s.property.numeric(),
			title: s.property.string(),
			authors: s.property.array(s.property.model(() => Account.model)),
			text: s.property.string(),
			evaluation: s.property.decimal(),
			tags: s.property.array(s.property.object({ name: s.property.string() })),
			comments: s.property.array(s.property.model(() => ArticleComment.model)),
		},
	});

	id: number;
	title: string;
	authors: Account[];
	text: string;
	evaluation: number;
	tags: { name: string }[];
	comments: ArticleComment[];
}

/**
 * Test class of a comment on an article.
 */
class ArticleComment
{
	static model = s.defineModel({
		Class: ArticleComment,
		identifier: "id",
		properties: {
			id: s.property.numeric(),
			article: s.property.model(circular<Article>(() => Article.model)),
			author: s.property.model(() => Account.model),
			message: s.property.string(),
		},
	});

	id: number;
	article?: Article;
	author: Account;
	message: string;
}

/**
 * Get a test account instance.
 */
function getTestAccount(): Account
{
	const account = new Account();
	account.id = 52;
	account.createdAt = new Date();
	account.name = "John Doe";
	account.email = "john@doe.test";
	account.active = true;
	return account;
}

function getTestArticle(): Article
{
	const article = new Article();
	article.id = 1;
	article.title = "this is a test";
	article.text = "this is a long test.";
	article.evaluation = 25.23;
	article.tags = [
		{ name: "test" },
		{ name: "foo" },
	];
	article.authors = [getTestAccount()];
	article.comments = [];
	return article;
}

describe("model", () => {
	it("initializes a new model", () => {
		const article = getTestArticle();
		const newModel = Article.model.model(article);
		expect(newModel.instance).toBe(article);
	});
	it("gets a model state from its instance", () => {
		const article = getTestArticle();
		expect(Article.model.model(article).isNew()).toBeTruthy();
		expect(Article.model.model(article).isDirty()).toBeFalsy();
	});
	it("gets a model identifier value", () => {
		const article = getTestArticle();
		expect(Article.model.model(article).getIdentifier()).toBe(1);
	});
	it("gets a model composite identifier value", () => {
		class CompositeModel
		{
			static model = s.defineModel({
				Class: CompositeModel,
				properties: {
					firstId: s.property.numeric(),
					secondId: s.property.numeric(),
					label: s.property.string(),
				},
				identifier: ["firstId", "secondId"],
			})

			firstId: number;
			secondId: number;
			label: string;
		}

		expect(
			CompositeModel.model.model(Object.assign(new CompositeModel(), {
				firstId: 5,
				secondId: 6,
				label: "test",
			})).getIdentifier()
		).toStrictEqual([5, 6]);
	});
	it("checks model dirtiness when altered, then reset diff", () => {
		const article = getTestArticle();
		expect(Article.model.model(article).isDirty()).toBeFalsy();
		article.title = "new title";
		expect(Article.model.model(article).isDirty()).toBeTruthy();
		Article.model.model(article).resetDiff()
		expect(Article.model.model(article).isDirty()).toBeFalsy();
	});

	it("deserializes a model from a serialized form", () => {
		const expectedArticle = Object.assign(new Article(), {
			id: 1,
			title: "this is a test",
			authors: [
				Object.assign(new Account(), { id: 52, name: "John Doe", email: "test@test.test", createdAt: new Date("2022-08-07T08:47:01.000Z"), active: true, }),
				Object.assign(new Account(), { id: 4, name: "Tester", email: "another@test.test", createdAt: new Date("2022-09-07T18:32:55.000Z"), active: false, }),
			],
			text: "this is a long test.",
			evaluation: 8.52,
			tags: [ {name: "test"}, {name: "foo"} ],
			comments: [
				Object.assign(new ArticleComment(), { id: 542, author: Object.assign(new Account(), { id: 52, name: "John Doe", email: "test@test.test", createdAt: new Date("2022-08-07T08:47:01.000Z"), active: true, }), message: "comment content", }),
			],
		});

		const deserializedArticle = Article.model.parse({
			id: 1,
			title: "this is a test",
			authors: [
				{ id: 52, name: "John Doe", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, },
				{ id: 4, name: "Tester", email: "another@test.test", createdAt: "2022-09-07T18:32:55.000Z", active: false, },
			],
			text: "this is a long test.",
			evaluation: "8.52",
			tags: [ {name: "test"}, {name: "foo"} ],
			comments: [
				{ id: 542, author: { id: 52, name: "John Doe", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, }, message: "comment content", },
			],
		});

		const deserializedArticleProperties = Article.model.model(deserializedArticle).getInstanceProperties();
		delete deserializedArticleProperties.authors[0]._sharkitek;
		delete deserializedArticleProperties.authors[1]._sharkitek;
		delete deserializedArticleProperties.comments[0]._sharkitek;
		delete (deserializedArticleProperties.comments[0].author as any)._sharkitek;
		const expectedArticleProperties = Article.model.model(expectedArticle).getInstanceProperties();
		delete expectedArticleProperties.authors[0]._sharkitek;
		delete expectedArticleProperties.authors[1]._sharkitek;
		delete expectedArticleProperties.comments[0]._sharkitek;
		delete (expectedArticleProperties.comments[0].author as any)._sharkitek;
		expect(deserializedArticleProperties).toEqual(expectedArticleProperties);
	});

	it("serializes an initialized model", () => {
		const article = getTestArticle();
		expect(Article.model.model(article).serialize()).toEqual({
			id: 1,
			title: "this is a test",
			text: "this is a long test.",
			evaluation: "25.23",
			tags: [{ name: "test" }, { name: "foo" }],
			authors: [
				{ id: 52, createdAt: article.authors[0].createdAt.toISOString(), name: "John Doe", email: "john@doe.test", active: true }
			],
			comments: [],
		});
	});

	it("deserializes, changes and patches", () => {
		const deserializedArticle = Article.model.parse({
			id: 1,
			title: "this is a test",
			authors: [
				{ id: 52, name: "John Doe", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, },
				{ id: 4, name: "Tester", email: "another@test.test", createdAt: "2022-09-07T18:32:55.000Z", active: false, },
			],
			text: "this is a long test.",
			evaluation: "8.52",
			tags: [ {name: "test"}, {name: "foo"} ],
			comments: [
				{ id: 542, author: { id: 52, name: "John Doe", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, }, message: "comment content", },
			],
		});

		deserializedArticle.text = "A new text for a new life!";

		expect(Article.model.model(deserializedArticle).patch()).toStrictEqual({
			id: 1,
			text: "A new text for a new life!",
		});

		deserializedArticle.evaluation = 5.24;

		expect(Article.model.model(deserializedArticle).patch()).toStrictEqual({
			id: 1,
			evaluation: "5.24",
		});
	});

	it("patches with modified submodels", () => {
		const deserializedArticle = Article.model.parse({
			id: 1,
			title: "this is a test",
			authors: [
				{ id: 52, name: "John Doe", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, },
				{ id: 4, name: "Tester", email: "another@test.test", createdAt: "2022-09-07T18:32:55.000Z", active: false, },
			],
			text: "this is a long test.",
			evaluation: "8.52",
			tags: [ {name: "test"}, {name: "foo"} ],
			comments: [
				{ id: 542, author: { id: 52, name: "John Doe", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, }, message: "comment content", },
			],
		});

		deserializedArticle.authors[1].active = true;

		expect(Article.model.model(deserializedArticle).patch()).toStrictEqual({
			id: 1,
			authors: [
				{ id: 52, },
				{ id: 4, active: true },
			],
		});

		deserializedArticle.comments[0].author.name = "Johnny";

		expect(Article.model.model(deserializedArticle).patch()).toStrictEqual({
			id: 1,
			comments: [
				{
					id: 542,
					author: {
						id: 52,
						name: "Johnny",
					},
				},
			],
		});
	});

	it("deserializes and patches with fields that are not properties", () => {
		class TestModel
		{
			static model = defineModel({
				Class: TestModel,
				properties: {
					id: s.property.numeric(),
					label: s.property.string(),
				},
				identifier: "id",
			})

			id: number;
			label: string;

			notAProperty: { hello: string } = { hello: "world" };
		}

		const deserializedModel = TestModel.model.parse({
			id: 5,
			label: "testing",
		});
		expect(deserializedModel.id).toBe(5);
		expect(deserializedModel.label).toBe("testing");
		expect(deserializedModel.notAProperty?.hello).toBe("world");

		const clonedDeserializedModel = TestModel.model.model(deserializedModel).clone();

		deserializedModel.label = "new!";
		expect(TestModel.model.model(deserializedModel).patch()).toStrictEqual({ id: 5, label: "new!" });

		deserializedModel.notAProperty.hello = "monster";
		expect(TestModel.model.model(deserializedModel).patch()).toStrictEqual({ id: 5 });

		expect(TestModel.model.model(deserializedModel).serialize()).toStrictEqual({ id: 5, label: "new!" });

		expect(TestModel.model.model(clonedDeserializedModel).serialize()).toStrictEqual({ id: 5, label: "testing" });
		expect(clonedDeserializedModel.notAProperty.hello).toEqual("world");
	});
});