import * as assert from "assert";
import * as fs from "fs/promises";
import { TextlintResult } from "@textlint/kernel";
import { TesterErrorDefinition } from "./textlint-tester";

export type TestTextlintLinter = {
    lintText(text: string, ext: string): Promise<TextlintResult>;
    lintFile(filePath: string): Promise<TextlintResult>;
};

type TestTestArgs =
    | {
          inputPath: string;
      }
    | {
          text: string;
          ext: string;
      };

/**
 * validate and get text
 * @returns {string}
 */
async function getTestText(args: TestTestArgs) {
    if ("inputPath" in args && typeof args.inputPath === "string") {
        return fs.readFile(args.inputPath, "utf-8");
    }
    if ("text" in args && typeof args.text === "string") {
        return args.text;
    }
    throw new Error("should be defined { text } or { inputPath }");
}

export type InvalidPatternArgs =
    | {
          textlint: TestTextlintLinter;
          inputPath: string;
          errors: TesterErrorDefinition[];
          description?: string;
      }
    | {
          textlint: TestTextlintLinter;
          errors: TesterErrorDefinition[];
          text: string;
          ext: string;
          description?: string;
      };

/**
 * Test invalid pattern
 * @param args
 */
export async function testInvalid(args: InvalidPatternArgs) {
    const textlint = args.textlint;
    const errors = args.errors;
    const actualText = await getTestText(args);
    const lines = actualText.split(/\n/);
    assert.strictEqual(
        typeof actualText,
        "string",
        `invalid property should have text string
e.g.)
invalid : [
    {
        text: "example text",
        errors: [{
            message: "expected message"
        }]
    }
]
`
    );
    assert.ok(
        Array.isArray(errors),
        `invalid property should have array of expected error
e.g.)
invalid : [
    {
        text: "example text",
        errors: [{
            message: "expected message"
        }]
    }
]
            `
    );
    const errorLength = errors.length;
    let promise: Promise<TextlintResult>;
    if ("inputPath" in args && args.inputPath !== undefined) {
        promise = textlint.lintFile(args.inputPath);
    } else if ("text" in args && args.text !== undefined) {
        const { text, ext } = args;
        promise = textlint.lintText(text, ext);
    } else {
        throw new Error("Should set `text` or `inputPath`");
    }
    return promise.then((lintResult) => {
        const descriptionArea = args.description
            ? `
===Description===:
${args.description}
`
            : ``;
        assert.strictEqual(
            lintResult.messages.length,
            errorLength,
            `invalid: should have ${errorLength} errors but had ${lintResult.messages.length}:${descriptionArea}
===Text===:
${actualText}

==Result==:
${JSON.stringify(lintResult, null, 4)}`
        );
        errors.forEach((error, errorIndex) => {
            const { ruleId, message, line, column, index, range, loc } = error;
            const resultMessageObject = lintResult.messages[errorIndex];
            // check
            assert.ok(
                resultMessageObject.line >= 1,
                `lint result's line number is ${resultMessageObject.line}, should be over than 1.`
            );
            assert.ok(
                resultMessageObject.line <= lines.length,
                `lint result's line number is line:${resultMessageObject.line}, but total line number of the text is ${lines.length}.
The result's line number should be less than ${lines.length}`
            );
            const columnText = lines[resultMessageObject.line - 1];
            assert.ok(
                resultMessageObject.column >= 1,
                `lint result's column number is ${resultMessageObject.column}, should be over than 1.`
            );
            assert.ok(
                resultMessageObject.column <= columnText.length + 1,
                `lint result's column number is ${resultMessageObject.column},` +
                    `but the length of the text @ line:${resultMessageObject.line} is ${columnText.length + 1}.
The result's column number should be less than ${columnText.length + 1}`
            );
            if (ruleId !== undefined) {
                const resultRuleId = resultMessageObject.ruleId;
                assert.strictEqual(resultRuleId, ruleId, `"ruleId should be "${ruleId}"`);
            }
            if (message !== undefined) {
                const resultMessage = resultMessageObject.message;
                assert.strictEqual(resultMessage, message, `"message should be "${message}"`);
            }
            if (line !== undefined) {
                const resultLine = resultMessageObject.line;
                assert.strictEqual(resultLine, line, `line should be ${line}`);
            }
            if (column !== undefined) {
                const resultColumn = resultMessageObject.column;
                assert.strictEqual(resultColumn, column, `"column should be ${column}`);
            }
            if (index !== undefined) {
                const resultIndex = resultMessageObject.index;
                assert.strictEqual(resultIndex, index, `"index should be ${index}`);
            }
            if (range !== undefined) {
                const resultRange = resultMessageObject.range;
                assert.deepStrictEqual(resultRange, range, `"range should be ${JSON.stringify(range, null, 4)}`);
            }
            if (loc !== undefined) {
                const resultLoc = resultMessageObject.loc;
                assert.deepStrictEqual(resultLoc, loc, `"loc should be ${JSON.stringify(loc, null, 4)}`);
            }
            if (error.suggestions !== undefined) {
                const resultSuggestions = resultMessageObject.suggestions;
                assert.ok(Array.isArray(resultSuggestions), `suggestions should be an array`);
                assert.strictEqual(
                    resultSuggestions.length,
                    error.suggestions.length,
                    `suggestions length should be ${error.suggestions.length}`
                );
                error.suggestions.forEach((expectedSuggestion, suggestionIndex) => {
                    const resultSuggestion = resultSuggestions[suggestionIndex];
                    assert.strictEqual(
                        resultSuggestion.id,
                        expectedSuggestion.id,
                        `suggestions[${suggestionIndex}].id should be "${expectedSuggestion.id}"`
                    );
                    if (expectedSuggestion.message !== undefined) {
                        assert.strictEqual(
                            resultSuggestion.message,
                            expectedSuggestion.message,
                            `suggestions[${suggestionIndex}].message should be "${expectedSuggestion.message}"`
                        );
                    }
                    if (expectedSuggestion.range !== undefined) {
                        assert.deepStrictEqual(
                            resultSuggestion.fix.range,
                            expectedSuggestion.range,
                            `suggestions[${suggestionIndex}].fix.range should be ${JSON.stringify(
                                expectedSuggestion.range,
                                null,
                                4
                            )}`
                        );
                    }
                    if (expectedSuggestion.output !== undefined) {
                        assert.strictEqual(
                            resultSuggestion.fix.text,
                            expectedSuggestion.output,
                            `suggestions[${suggestionIndex}].fix.text should be "${expectedSuggestion.output}"`
                        );
                    }
                });
            }
        });
    });
}

export type ValidPatternArgs =
    | {
          textlint: TestTextlintLinter;
          inputPath: string;
          description?: string;
      }
    | {
          textlint: TestTextlintLinter;
          text: string;
          ext: string;
          description?: string;
      };

export async function testValid(args: ValidPatternArgs) {
    const { textlint } = args;
    const actualText = await getTestText(args);
    assert.strictEqual(typeof actualText, "string", "valid should has string or { text }.");
    let promise: Promise<TextlintResult>;
    if ("inputPath" in args && args.inputPath !== undefined) {
        promise = textlint.lintFile(args.inputPath);
    } else if ("text" in args && args.text !== undefined) {
        promise = textlint.lintText(args.text, args.ext);
    } else {
        throw new Error("Should set `text` or `inputPath`");
    }
    return promise.then((results) => {
        const descriptionArea = args.description
            ? `
===Description===:
${args.description}
`
            : ``;
        assert.strictEqual(
            results.messages.length,
            0,
            `valid: should have no errors but had Error results:${descriptionArea}
===Text===:
${actualText}

==Result==:
${JSON.stringify(results, null, 4)}`
        );
    });
}
