import LoggingUtils from '@impl/utils/LoggingUtils';
import { Page } from '@playwright/test';
import { TestInfo } from '@business-logic/types/Test';
import { TestInfoError } from "@playwright/test";
import { DateTime } from 'luxon';
import { expect as pw_expect } from '@playwright/test';

export default class Soft {
    private static _instance: Soft | null = null;
    private testInfo: TestInfo;
    private page: Page;
    private failedAssertions: string[] = [];
    private errorCounter: number = 0;
    private errorsOnly: boolean = false;

    private constructor(testInfo: TestInfo, page: Page) {
        this.testInfo = testInfo;
        this.page = page;
    }

    public static initialize(testInfo: TestInfo, page: Page): void {
        Soft._instance = new Soft(testInfo, page);
    }

    public static setOptions(options: { errorsOnly: boolean }): void {
        Soft.getInstance().errorsOnly = options.errorsOnly;
    }

    public static getOptions(): { errorsOnly: boolean } {
        return { errorsOnly: Soft.getInstance().errorsOnly };
    }

    private static getInstance(): Soft {
        if (!Soft._instance)
            throw new Error("Soft is not initialized. Call Soft.initialize(testInfo, page) first!");
        return Soft._instance;
    }

    public async handleAssertion(
        matcherFull: string,
        matcherDisplay: string,
        matcherFunction: () => Promise<void>,
        reason?: string): Promise<void> {
        reason = reason ? reason : ''
        const reasonText = reason ? `'${reason}' ` : '';
        try {
            await matcherFunction();
            if (!this.errorsOnly)
                console.log(LoggingUtils.logValidate(`Validation ${reasonText}passed: ${matcherDisplay}!`, true));
        } catch (error) {
            console.log(LoggingUtils.logValidate(`Validation ${reasonText}failed: ${matcherDisplay}!`, false));
            //const errorMessage = error instanceof Error ? error.message : String(error);
            this.failedAssertions.push(`\n${++this.errorCounter}_Validation ${reasonText}failed:\n${LoggingUtils.replaceEmptyLinesWithMiddleDot(matcherFull)}!\n${error.stack}\n`);
            // NOTE: I find it more helpful for the stack trace to be included here, so we know which line the validation is failing
            try {
                const screenshot: Buffer = await this.page.screenshot({ fullPage: true });
                const name: string = LoggingUtils.sanitizeFileName(`${this.errorCounter}_Validation_${reason}${DateTime.now().valueOf()}`);
                await this.testInfo.attach(name, {
                    body: screenshot,
                    contentType: 'image/png'
                });
            } catch(err){
                // ohwell
                console.error(error)
            }
            
        }
    }

    public static expect(value: any, reason?: string): ExpectationChain {
        return new ExpectationChain(value, Soft.getInstance(), reason);
    }

    public static getFailedAssertions(): string[] {
        return Soft.getInstance().failedAssertions;
    }

    public static hasFailedAssertions(): boolean {
        return Soft.getInstance().failedAssertions.length > 0;
    }

    public static getFailureCount(): number {
        return Soft.getInstance().failedAssertions.length;
    }

    public static clearFailedAssertions(): void {
        Soft.getInstance().failedAssertions = [];
    }
}

export class SoftError implements TestInfoError {
    public readonly message?: string | undefined;
    constructor(msg: string) {
        this.message = msg;
    }
}

export class ExpectationChain {
    constructor(private value: any, private soft: Soft, private reason?: string) { }

    private formatValue(value: any): string {
        return LoggingUtils.truncateString(value);
    }

    public async toBe(expected: any): Promise<ExpectationChain> {
        await this.soft.handleAssertion(
            `expect(${this.value}).toBe(${expected})`,
            `expect(${this.formatValue(this.value)}).toBe(${this.formatValue(expected)})`,
            async () => await pw_expect(this.value).toBe(expected),
            this.reason
        );
        return this;
    }

    public async toEqual(expected: any): Promise<ExpectationChain> {
        await this.soft.handleAssertion(
            `expect(${JSON.stringify(this.value)}).toEqual(${expected})`,
            `expect(${this.formatValue(JSON.stringify(this.value))}).toEqual(${this.formatValue(expected)})`,
            async () => await pw_expect(this.value).toEqual(expected),
            this.reason
        );
        return this;
    }

    public async toContain(expected: any): Promise<ExpectationChain> {
        await this.soft.handleAssertion(
            `expect(${this.value}).toContain(${expected})`,
            `expect(${this.formatValue(this.value)}).toContain(${this.formatValue(expected)})`,
            async () => await pw_expect(this.value).toContain(expected),
            this.reason
        );
        return this;
    }

    public async toHaveText(expected: string): Promise<ExpectationChain> {
        await this.soft.handleAssertion(
            `expect(${this.value}).toHaveText(${expected})`,
            `expect(${this.formatValue(this.value)}).toHaveText(${this.formatValue(expected)})`,
            async () => {
                if (typeof this.value.textContent !== 'function') {
                    throw new Error('value does not have a textContent method');
                }
                const text = await this.value.textContent();
                await pw_expect(text).toHaveText(expected);
            }, this.reason
        );
        return this;
    }

    public async toBeGreaterThan(expected: number): Promise<ExpectationChain> {
        await this.soft.handleAssertion(
            `expect(${this.value}).toBeGreaterThan(${expected})`,
            `expect(${this.formatValue(this.value)}).toBeGreaterThan(${this.formatValue(expected)})`,
            async () => await pw_expect(this.value).toBeGreaterThan(expected),
            this.reason
        );
        return this;
    }

    public async toBeTruthy(): Promise<ExpectationChain> {
        await this.soft.handleAssertion(
            `expect(${this.value}).toBeTruthy`,
            `expect(${this.formatValue(this.value)}).toBeTruthy`,
            async () => await pw_expect(this.value).toBeTruthy(),
            this.reason
        );
        return this;
    }

    public async toBeFalsy(): Promise<ExpectationChain> {
        await this.soft.handleAssertion(
            `expect(${this.value}).toBeFalsy`,
            `expect(${this.formatValue(this.value)}).toBeFalsy`,
            async () => await pw_expect(this.value).toBeFalsy(),
            this.reason
        );
        return this;
    }
}