import { expect, Page } from "@playwright/test";
import { NodeSSH } from "node-ssh";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";

type UninstallOptions = {
    strict?: boolean;
    quickCheckTimeoutMs?: number;
};

const __applicationDir = path.dirname(fileURLToPath(import.meta.url));
const helloWorldPackagePath = path.resolve(__applicationDir, "../test-assets/hello-world-0.1.0.tgz");

function getHelloWorldPackagePath() {
    return helloWorldPackagePath;
}

async function goToMarketplacePackageManagement(page: Page) {
    await page.getByText("应用市场", { exact: true }).click();
    await page.getByRole("link", { name: "仓库配置" }).click();
    await expect(page).toHaveURL(/\/container_platform\/appMarket\/stash\/wareHouse$/, { timeout: 60_000 });
    await page.getByRole("tab", { name: "包管理" }).click();
}

async function prepareHelloWorldPackageLikeMarketplace(page: Page) {
    const packagePath = getHelloWorldPackagePath();
    if (!fs.existsSync(packagePath)) {
        throw new Error(`hello-world package not found at ${packagePath}`);
    }
    await goToMarketplacePackageManagement(page);
    await page.getByRole("button", { name: "添加包" }).click();
    await page.locator("input[type='file']").setInputFiles(packagePath);
    await expect(page.getByText("hello-world-0.1.0.tgz")).toBeVisible({ timeout: 120_000 });
    await page.getByRole("button", { name: "确 定" }).click();
    await expect(page.getByText("文件上传成功")).toBeVisible({ timeout: 120_000 });
    return packagePath;
}


function escapeRegExp(text: string) {
    return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

async function selectAppMarketSource(page: Page, source: string) {
    const defaultSource = page.getByRole("checkbox", { name: "openFuyao" }).first();
    if (source !== "openFuyao" && (await defaultSource.count()) > 0 && (await defaultSource.isVisible().catch(() => false))) {
        await defaultSource.uncheck().catch(() => {});
    }
    const sourceCheckbox = page.getByRole("checkbox", { name: source }).first();
    if ((await sourceCheckbox.count()) > 0 && (await sourceCheckbox.isVisible().catch(() => false))) {
        await sourceCheckbox.check();
    }
}

async function goToAppMarket(page: Page) {
    const origin = (() => {
        try {
            return new URL(page.url()).origin;
        } catch {
            return "";
        }
    })();

    if (origin) {
        await page.goto(origin + "/container_platform/appMarket/marketCategory");
    } else {
        await page.getByText("应用市场", { exact: true }).click();
    }

    await expect(page).toHaveURL(/\/container_platform\/appMarket\/marketCategory/, { timeout: 60_000 });
}

// 对应在应用市场安装组件
async function installComponent(page: Page, componentName: string, appName: string, version?: string, source = "openFuyao") {
    const origin = (() => {
        try {
            return new URL(page.url()).origin;
        } catch {
            return "";
        }
    })();
    if (origin) {
        await page.goto(origin + "/container_platform/appMarket/marketCategory");
    }
    await page.getByRole("link", { name: "应用列表" }).click();
    const searchBox = page.getByRole('textbox', { name: /搜索.*名称/ }).first();
    await expect(searchBox).toBeVisible({ timeout: 60_000 });
    if (source) {
        await selectAppMarketSource(page, source);
    }
    await searchBox.fill(componentName);
    await page.getByRole('button', { name: 'search' }).click();
    const componentTitle = page
        .locator("#root_container .application_item_title")
        .filter({ hasText: new RegExp(`^${escapeRegExp(componentName)}$`) })
        .first();
    await expect(componentTitle).toBeVisible({ timeout: 60_000 });
    await componentTitle.click();
    if (version) {
        await page.waitForTimeout(10_000);
        const selectedVersion = page.getByText(/版本:\s*/).first();
        await expect(selectedVersion).toBeVisible({ timeout: 60_000 });
        await selectedVersion.click();
        await page.getByText(`版本: ${version}`, { exact: true }).click();
    }
    await page.getByRole('button', { name: '部 署' }).click();
    // 某些时候组件在点击“部署”后会弹出风险确认框
    const appNameInput = page.getByRole('textbox', { name: '* 应用名称' });
    const deployRiskCheckbox = page.getByRole('checkbox', { name: /我已了解并确认上述风险/ }).first();
    if ((await deployRiskCheckbox.count()) > 0 && (await deployRiskCheckbox.isVisible().catch(() => false))) {
        await deployRiskCheckbox.check();
        await page.getByRole('button', { name: /继续部署/ }).first().click();
    }
    await appNameInput.fill(appName);
    const namespaceCombobox = page.getByRole('combobox', { name: /命名空间/i }).first();
    await expect(namespaceCombobox).toBeVisible({ timeout: 60_000 });
    await namespaceCombobox.click({ force: true });
    const namespaceListbox = page.locator('#form_in_modal_nameSpace_list');
    if (!(await namespaceListbox.isVisible().catch(() => false))) {
        await namespaceCombobox.focus();
        await page.keyboard.press('ArrowDown');
    }
    const defaultNamespaceOption = page.getByTitle('bke-cluster').first();
    await expect(defaultNamespaceOption).toBeVisible({ timeout: 10_000 });
    await defaultNamespaceOption.click();
    const confirmButton = page.getByRole('button', { name: /确\s*定/ });
    await expect(confirmButton).toBeVisible({ timeout: 60_000 });
    await expect(confirmButton).toBeEnabled({ timeout: 60_000 });
    await confirmButton.click();
};

// 检查组件部署成功
async function checkComponent(
    page: Page,
    appName: string,
    options?: {
        entry?: "extendManage" | "applicationManageHelm";
        serviceClusterUrl?: string;
    },
) {
    const entry = options?.entry ?? "extendManage";
    if (entry === "applicationManageHelm") {
        await expect(page.getByText("部署成功,即将跳转应用管理页面")).toBeVisible({ timeout: 120_000 });
        await expect(page.getByRole('navigation').getByText('应用管理')).toBeVisible();
        await page.getByRole('textbox', { name: /搜索.*名称/ }).fill(appName);
        await page.getByRole('button', { name: 'search' }).click();
        await expect(page.getByRole('cell', { name: 'check-circle 部署成功' })).toBeVisible();
    } else {
        await expect(page.getByText('部署成功,即将跳转扩展组件管理页面')).toBeVisible({ timeout: 120_000 });
        await expect(page.getByRole('navigation').getByText('扩展组件管理')).toBeVisible();
        await page.getByRole('textbox', { name: /搜索.*名称/ }).fill(appName);
        await page.getByRole('button', { name: 'search' }).click();
        await expect(page.getByRole('cell', { name: 'check-circle 部署成功' })).toBeVisible();
    }
};

// 对应将yaml文件中的单一的 oldStr 修改为 newStr(修改整行)
async function changeString(page: Page, oldStr: string, newStr: string) {
    await page.getByText(oldStr).first().click();
    await page.keyboard.press('Shift+Home');
    await page.keyboard.press('Backspace');
    await page.keyboard.type(newStr);
    // 验证修改后的文本出现
    await expect(page.getByText(newStr).first()).toBeVisible();
}

type HelmRepoInfo = { name: string; url: string };

async function changePullPolicyLineWithWait(page: Page): Promise<{ oldLine: string; newLine: string }> {
    let lineLocator = page.getByText(/pullPolicy:\s*(Always|IfNotPresent)/).first();
    let ready = false;
    for (let i = 0; i < 12; i++) {
        await page.getByRole("tab", { name: "Values.yaml" }).click().catch(() => {});
        lineLocator = page.getByText(/pullPolicy:\s*(Always|IfNotPresent)/).first();
        const found = await lineLocator
            .waitFor({ state: "visible", timeout: 5000 })
            .then(() => true)
            .catch(() => false);
        if (found) {
            ready = true;
            break;
        }
    }

    expect(ready, "Values.yaml 未加载出 pullPolicy 字段").toBeTruthy();
    await expect(lineLocator).toBeVisible({ timeout: 60_000 });
    const raw = ((await lineLocator.textContent()) ?? "").replace(/\u00a0/g, " ");
    const singleLine = raw.replace(/\r?\n/g, "").trimEnd();
    const matched = singleLine.match(/^(\s*)pullPolicy:\s*(Always|IfNotPresent)\s*$/);
    expect(matched, "无法解析 pullPolicy 行").not.toBeNull();
    const indent = matched![1];
    const current = matched![2];
    const next = current === "Always" ? "IfNotPresent" : "Always";
    const oldLine = `${indent}pullPolicy: ${current}`;
    const newLine = `${indent}pullPolicy: ${next}`;
    await lineLocator.click();
    await page.keyboard.press("Shift+Home");
    await page.keyboard.press("Backspace");
    await page.keyboard.type(newLine);
    await expect(page.getByText(new RegExp(`pullPolicy:\\s*${next}`)).first()).toBeVisible({ timeout: 60_000 });
    return { oldLine, newLine };
}

// 对应在日志界面进行筛选操作
async function selectLog(page: Page, defaultContainer: string, selectContainer: string) {
    await page.getByRole('tab', { name: '日志' }).click();
    await page.getByText(defaultContainer + '-').click();
    await page.getByTitle(selectContainer + '-').locator('div').click();
    await page.getByRole('button', { name: /查\s*询/ }).click();
}

// 等待日志数据就绪
async function waitForLogReady(page: Page) {
    const emptyLogLocator = page.getByText("暂无日志");
    await expect
        .poll(async () => {
            const hasEmptyLog = await emptyLogLocator
                .count()
                .then((count: number) => count > 0 && emptyLogLocator.first().isVisible())
                .catch(() => false);
            if (!hasEmptyLog) {
                return true;
            }
            await page.getByRole("button", { name: /查\s*询/ }).first().click().catch(() => {});
            return false;
        }, {
            timeout: 60_000,
            intervals: [1000, 2000, 5000],
        })
        .toBeTruthy();
}

// 日志导出并校验下载文件
async function verifyLogExport(page: Page, outputDir: string) {
    await waitForLogReady(page);
    const exportBtn = page.getByText("导出").first();
    const exportIcon = page.getByRole("img", { name: "export" }).locator("svg");
    const triggerExport = async () => {
        if (await exportBtn.count()) {
            await exportBtn.click();
        } else {
            await exportIcon.click();
        }
    };
    const download = await Promise.all([
        page.waitForEvent("download", { timeout: 60_000 }),
        triggerExport(),
    ]).then(([d]) => d);
    await expect(page.getByText("导出成功")).toBeVisible({ timeout: 60_000 });
    const filename = download.suggestedFilename();
    expect(filename).toBeTruthy();
    const savePath = path.join(outputDir, filename);
    await download.saveAs(savePath);
    expect(fs.statSync(savePath).size).toBeGreaterThan(0);
}


// 对应在扩展组件管理中卸载组件
async function uninstallComponent(page: Page, appName: string, opts: UninstallOptions = {}) {
    const { strict = true, quickCheckTimeoutMs = 3_000 } = opts;
    await page.getByRole('textbox', { name: /搜索.*名称/ }).fill(appName);
    await page.getByRole('button', { name: 'search' }).click();
    const targetRow = page.getByRole('row').filter({ hasText: appName }).first();

    if (!strict) {
        const start = Date.now();
        let found = false;
        while (Date.now() - start < quickCheckTimeoutMs) {
            found = (await targetRow.count()) > 0;
            if (found) break;
            await page.waitForTimeout(300);
        }
        if (!found) return false;
    } else {
        await expect(targetRow).toBeVisible({ timeout: quickCheckTimeoutMs });
    }

    await targetRow.locator('[data-icon="more"]').first().click();
    await page.getByRole('button', { name: '卸载' }).click();
    await expect(page.getByText('卸载扩展组件', { exact: true })).toBeVisible();
    await page.getByRole('checkbox', { name: '已了解卸载操作无法恢复' }).check();
    await page.getByRole('button', { name: '卸 载' }).click();
    await expect(page.getByText('扩展组件' + appName + '卸载成功')).toBeVisible();
    await expect(page.getByRole('cell', { name: 'Simple Empty 暂无数据' })).toBeVisible();
    return true;
};

// 对应在应用管理中卸载 Helm 应用
async function uninstallHelmComponent(page: Page, appName: string, opts: UninstallOptions = {}) {
    const { strict = true, quickCheckTimeoutMs = 3_000 } = opts;
    await page.getByRole('textbox', { name: /搜索.*名称/ }).fill(appName);
    await page.getByRole('button', { name: 'search' }).click();
    const targetRow = page.getByRole('row').filter({ hasText: appName }).first();

    if (!strict) {
        const start = Date.now();
        let found = false;
        while (Date.now() - start < quickCheckTimeoutMs) {
            found = (await targetRow.count()) > 0;
            if (found) break;
            await page.waitForTimeout(300);
        }
        if (!found) return false;
    } else {
        await expect(targetRow).toBeVisible({ timeout: quickCheckTimeoutMs });
    }

    await targetRow.locator('[data-icon="more"]').first().click();
    await page.getByRole('button', { name: '卸载' }).click();
    await expect(page.getByText('卸载应用', { exact: true })).toBeVisible();
    await page.getByRole('checkbox', { name: '已了解卸载操作无法恢复' }).check();
    await page.getByRole('button', { name: '卸 载' }).click();
    await page.getByRole('textbox', { name: /搜索.*名称/ }).fill(appName);
    await page.getByRole('button', { name: 'search' }).click();
    await expect(page.getByRole('row').filter({ hasText: appName })).toHaveCount(0);
    return true;
}

// 对应在详情页面进行升级操作
async function upgradeComponent(page: Page, initVersion: string, upgradeVersion: string) {
    await page.getByRole('list').getByText('详情').click();
    await page.getByRole('button', { name: '操作 down' }).click();
    await page.getByRole('button', { name: '升级' }).click();
    const initVersionLoc = page.getByTitle(initVersion, { exact: true });
    await expect(initVersionLoc).toBeVisible({ timeout: 60_000 });
    await initVersionLoc.click();
    await page.getByTitle(upgradeVersion, { exact: true }).click();
};

// 对应在详情页面进行回退操作(在升级成功界面后操作)
async function revertComponent(page: Page, revertName: string) {
    const actionButton = page.getByRole('button', { name: '操作 down' }).first();
    const detailLink = page.getByRole('link', { name: '详情' }).first();
    const detailListItem = page.getByRole('list').getByText('详情').first();
    if ((await actionButton.count()) === 0) {
        if ((await detailLink.count()) > 0) {
            await detailLink.click();
        } else if ((await detailListItem.count()) > 0) {
            await detailListItem.click();
        }
    }

    await expect(actionButton).toBeVisible({ timeout: 60_000 });
    await page.getByRole('button', { name: '操作 down' }).click();
    await page.getByRole('button', { name: '回退' }).click();
    await expect(page.getByText('应用回退')).toBeVisible();
    await page.waitForTimeout(60_000);
    const revertRowFirstCell = page.getByRole('row', { name: revertName }).getByRole('cell').first();
    await expect(revertRowFirstCell).toBeVisible({ timeout: 60_000 });
    await revertRowFirstCell.click();
    await page.getByRole('button', { name: /确\s*定/ }).click();
    await expect(page.getByText('回退版本成功')).toBeVisible({ timeout: 60_000 });
};

// 对应检查pod 
async function checkPrefixResourceNum(page: Page, resource: string, prefix: string, state: string, stateNum: number) {
    await page.getByText('工作负载').click();
    await page.getByRole('link', { name: resource }).click();
    await page.getByRole('textbox', { name: /搜索.*名称/ }).fill(prefix);
    await page.getByRole('button', { name: 'search' }).click();
    await expect(page.getByText(state)).toHaveCount(stateNum);
};

// 进入扩展组件管理-应用详情-资源标签页
async function gotoApplicationResourceTab(page: Page, serviceClusterUrl: string, appName: string) {
    await page.goto(serviceClusterUrl + "/container_platform/extendManage");
    await page.getByRole("textbox", { name: /搜索.*名称/ }).fill(appName);
    await page.getByRole("button", { name: "search" }).click();
    await page.getByRole("link", { name: appName }).click();
    await page.getByRole("tab", { name: "资源" }).click();
    await expect(page.getByRole("table")).toBeVisible();
}

// 点击指定列头的排序按钮
async function clickSortHeader(page: Page, headerName: string) {
    const header = page.getByRole("columnheader", { name: new RegExp(headerName) }).first();
    await expect(header).toBeVisible();
    const before = await header.getAttribute("aria-sort");
    await header.click();
    // 等待排序完成:以 aria-sort 变化作为结束条件
    if (before) {
        await expect
            .poll(async () => await header.getAttribute("aria-sort"), { timeout: 60_000 })
            .not.toBe(before);
    } else {
        await expect.poll(async () => await header.getAttribute("aria-sort"), { timeout: 60_000 }).not.toBeNull();
    }
}

// 获取表格某列文本(按列号,1-based)
async function getColumnTexts(page: Page, columnIndex: number): Promise<string[]> {
    const cells = page.locator(`tbody tr td:nth-child(${columnIndex})`);
    const texts = await cells.allTextContents();
    return texts.map((t) => (t ?? "").trim()).filter((t) => t.length > 0);
}

// 断言升序
function expectSortedAsc(values: string[], message = "排序结果不是升序") {
    const normalized = values.map((v) => v.trim().toLowerCase());
    const sorted = [...normalized].sort((a, b) => a.localeCompare(b, "zh-Hans-CN"));
    expect(normalized, message).toEqual(sorted);
}

// 断言降序
function expectSortedDesc(values: string[], message = "排序结果不是降序") {
    const normalized = values.map((v) => v.trim().toLowerCase());
    const sorted = [...normalized].sort((a, b) => b.localeCompare(a, "zh-Hans-CN"));
    expect(normalized, message).toEqual(sorted);
}

// 断言默认排序逻辑(优先 aria-sort)
async function expectDefaultSortState(page: Page, headerName: string, sortState: "ascending" | "descending") {
    const header = page.getByRole("columnheader", { name: new RegExp(headerName) }).first();
    await expect(header).toHaveAttribute("aria-sort", sortState);
}

// 概览页排序校验(按名称列 + 更新时间列,首次点击升序、二次点击降序)
async function verifyOverviewSort(
    page: Page,
    options: { nameHeader: string; nameColIndex: number; updateTimeHeader: string; updateTimeColIndex: number },
) {
    const { nameHeader, nameColIndex, updateTimeHeader, updateTimeColIndex } = options;
    const rows = page.locator("table tbody tr");
    await expect.poll(async () => rows.count(), { timeout: 60_000 }).toBeGreaterThanOrEqual(2);
    await clickSortHeader(page, nameHeader);
    const nameAsc = await getColumnTexts(page, nameColIndex);
    expectSortedAsc(nameAsc, `${nameHeader}第一次点击后升序排序结果不正确`);
    await clickSortHeader(page, nameHeader);
    const nameDesc = await getColumnTexts(page, nameColIndex);
    expectSortedDesc(nameDesc, `${nameHeader}第二次点击后降序排序结果不正确`);
    await clickSortHeader(page, updateTimeHeader);
    const timeAsc = await getColumnTexts(page, updateTimeColIndex);
    expectSortedAsc(timeAsc, `${updateTimeHeader}第一次点击后升序排序结果不正确`);
    await clickSortHeader(page, updateTimeHeader);
    const timeDesc = await getColumnTexts(page, updateTimeColIndex);
    expectSortedDesc(timeDesc, `${updateTimeHeader}第二次点击后降序排序结果不正确`);
}

// 资源标签页排序校验(默认:资源名称在第1列,类型在第3列)
async function verifyResourceSortInResourceTab(page: Page, typeColumnIndex = 3) {
    const rows = page.locator("table tbody tr");
    await expect.poll(async () => rows.count(), { timeout: 60_000 }).toBeGreaterThanOrEqual(2);
    await clickSortHeader(page, "资源名称");
    const resourceNameAsc = await getColumnTexts(page, 1);
    expectSortedAsc(resourceNameAsc, "资源名称第一次点击后升序排序结果不正确");
    await clickSortHeader(page, "资源名称");
    const resourceNameDesc = await getColumnTexts(page, 1);
    expectSortedDesc(resourceNameDesc, "资源名称第二次点击后降序排序结果不正确");
    await clickSortHeader(page, "类型");
    const typeAsc = await getColumnTexts(page, typeColumnIndex);
    expectSortedAsc(typeAsc, "类型第一次点击后升序排序结果不正确");
    await clickSortHeader(page, "类型");
    const typeDesc = await getColumnTexts(page, typeColumnIndex);
    expectSortedDesc(typeDesc, "类型第二次点击后降序排序结果不正确");
}

const serviceClusterUrl = process.env.TEST_FUYAOURL;

async function openYamlQuickDeploy(page: any) {
    await page.goto(serviceClusterUrl + "/container_platform/appMarket/marketCategory");
    await page.getByRole("link", { name: "应用列表" }).click();
    await page.getByRole("img", { name: "appstore-add" }).locator("svg").click();
    await page.getByRole("button", { name: "前往部署" }).click();
    await expect(page).toHaveURL(/\/container_platform\/appMarket\/oneClickDeploy/);
    await expect(page.getByRole("button", { name: "部 署" })).toBeVisible();
}

async function inputYamlContent(page: any, yamlContent: string) {
    const genericTextbox = page.getByRole("textbox").first();
    await expect(genericTextbox).toBeVisible({ timeout: 60_000 });
    console.info("[yaml-input] strategy=textbox-fallback");
    await genericTextbox.click();
    await page.keyboard.press("Control+A");
    await page.keyboard.insertText(yamlContent);
}

async function deleteDeploymentByName(page: any, deploymentName: string) {
    if (serviceClusterUrl) {
        await page.goto(serviceClusterUrl + "/container_platform/workload/deployment");
    } else {
        await page.getByText("工作负载").click();
        await page.getByRole("link", { name: "Deployment", exact: true }).click();
    }
    await expect(page).toHaveURL(/\/container_platform\/workload\/deployment/);
    // Deployment 状态刷新存在延迟,先尝试刷新一次避免读到旧状态
    const refreshBtn = page.getByRole("button", { name: "sync" }).first();
    if ((await refreshBtn.count()) > 0) {
        await refreshBtn.click().catch(() => {});
    }
    const searchInputCandidates = [
        page.getByRole("textbox", { name: /搜索.*名称/ }).first(),
        page.getByRole("textbox").first(),
    ];
    let searchInput = searchInputCandidates[0];
    for (const candidate of searchInputCandidates) {
        if ((await candidate.count()) > 0) {
            searchInput = candidate;
            break;
        }
    }
    await expect(searchInput).toBeVisible({ timeout: 60_000 });
    const searchDeployment = async () => {
        await searchInput.fill(deploymentName);
        await searchInput.press("Enter");
        await page.waitForTimeout(500);
    };
    await searchDeployment();
    const targetNameCell = page.getByRole("cell", { name: deploymentName, exact: true }).first();
    await expect(targetNameCell).toBeVisible({ timeout: 60_000 });
    // 新创建后状态可能短暂 Failed,轮询直到状态收敛为可操作状态
    await expect
        .poll(
            async () => {
                if ((await refreshBtn.count()) > 0) {
                    await refreshBtn.click().catch(() => {});
                }
                await searchDeployment();
                const currentRow = page.getByRole("row").filter({ hasText: deploymentName }).first();
                const rowCount = await currentRow.count();
                if (rowCount === 0) return "";
                return (await currentRow.textContent()) ?? "";
            },
            { timeout: 60_000, message: `Deployment ${deploymentName} 未收敛到可操作状态` }
        )
        .toMatch(/Active|Running/);

    await page.getByRole("columnheader", { name: "操作" }).click().catch(() => {});
    const actionRow = page.getByRole("row", { name: deploymentName }).first();
    await expect(actionRow).toBeVisible({ timeout: 60_000 });
    const rowActionIcon = actionRow.locator('[data-icon="more"]').first();
    if ((await rowActionIcon.count()) > 0) {
        await rowActionIcon.click();
    } else {
        await actionRow.locator("svg").first().click();
    }
    await page.getByRole("button", { name: "删除" }).click();
    const deleteDialog = page.getByRole("dialog").last();
    const confirmCheckbox = deleteDialog.getByRole("checkbox", { name: "已了解删除操作无法恢复" });
    await expect(confirmCheckbox).toBeVisible({ timeout: 60_000 });
    await confirmCheckbox.check();
    await deleteDialog.getByRole("button", { name: "删 除" }).click();
    // 删除动作可能异步生效,轮询直到目标 deployment 消失
    await expect
        .poll(
            async () => {
                await searchDeployment();
                return await page.getByRole("cell", { name: deploymentName, exact: true }).count();
            },
            { timeout: 60_000, message: `Deployment ${deploymentName} 删除后仍存在` }
        )
        .toBe(0);
}

// =========================
// SSH + kubectl 对比工具
// =========================

const normalizeSpaces = (text: string) => (text ?? "").replace(/\s+/g, " ").trim();

async function withKubectlSsh<T>(fn: (ssh: NodeSSH) => Promise<T>): Promise<T> {
    const TEST_SSH_ADDRESS = process.env.TEST_NODE2_IP;
    const TEST_SSH_PASSWORD = process.env.TEST_NODE2_PASSWORD;
    const TEST_SSH_USERNAME = "root";
    expect(TEST_SSH_ADDRESS, "缺少环境变量 TEST_NODE2_IP").toBeTruthy();
    expect(TEST_SSH_PASSWORD, "缺少环境变量 TEST_NODE2_PASSWORD").toBeTruthy();
    const ssh = new NodeSSH();
    await ssh.connect({
        host: TEST_SSH_ADDRESS!,
        username: TEST_SSH_USERNAME,
        password: TEST_SSH_PASSWORD!,
    });

    try {
        return await fn(ssh);
    } finally {
        ssh.dispose();
    }
}

async function execKubectl(
    ssh: NodeSSH,
    command: string,
): Promise<{ stdout: string; stderr: string; code?: number }> {
    const result = await ssh.execCommand(command);
    const code = typeof result.code === "number" ? result.code : undefined;
    return {
        stdout: (result.stdout ?? "").toString(),
        stderr: (result.stderr ?? "").toString(),
        code,
    };
}

async function kubectlGetFirstPodByNamePrefix(
    ssh: NodeSSH,
    namespace: string,
    podNamePrefix: string,
): Promise<{ namespace: string; podName: string }> {
    const maxRetry = 30;
    const intervalMs = 2000;

    for (let i = 0; i < maxRetry; i++) {
        const { stdout, code } = await execKubectl(
            ssh,
            `kubectl get pods -n ${namespace} --no-headers -o custom-columns=NAME:.metadata.name`,
        );
        if (code === 0) {
            const names = stdout
                .trim()
                .split(/\s+/)
                .filter(Boolean);
            const match =
                names.find((n) => n.startsWith(podNamePrefix)) ??
                names.find((n) => n.includes(podNamePrefix));
            if (match) return { namespace, podName: match };
        }
        await new Promise((r) => setTimeout(r, intervalMs));
    }
    throw new Error(`未找到命名空间=${namespace} 下 pod 前缀=${podNamePrefix} 的 pod(尝试  ${maxRetry} 次后仍为空)`);
}

async function getUiLogText(page: any): Promise<string> {
    const candidates = [
        page.locator("pre").first(),
        page.locator("code").first(),
        page.locator("textarea").first(),
        page.locator('[class*="log"]').first(),
        page.locator('[class*="Log"]').first(),
    ];

    for (const loc of candidates) {
        if ((await loc.count()) === 0) continue;
        const t = await loc.textContent();
        if (t && normalizeSpaces(t).length > 0 && !normalizeSpaces(t).includes("暂无日志")) {
            return t;
        }
    }

    const bodyText = await page.locator("body").innerText();
    const emptyLoc = page.getByText("暂无日志");
    const emptyVisible = await emptyLoc.isVisible().catch(() => false);
    if (emptyVisible) return "";
    return bodyText;
}

async function verifyUiLogWithKubectl(
    page: any,
    ssh: NodeSSH,
    params: {
        kubectlNamespace: string;
        kubectlPodNamePrefix: string;
        kubectlContainerName?: string;
    },
) {
    const { namespace, podName } = await kubectlGetFirstPodByNamePrefix(
        ssh,
        params.kubectlNamespace,
        params.kubectlPodNamePrefix,
    );

    const isHeaderLikeLine = (line: string) => /Pod/.test(line) && /容器/.test(line);
    const extractMsgLikeFragments = (line: string): string[] => {
        const n = normalizeSpaces(line);
        if (!n || n.length < 8) return [];

        const out = new Set<string>();

        for (const key of ["msg", "message"]) {
            const m = n.match(new RegExp(`"${key}"\\s*:\\s*"([^"]+)"`));
            if (m?.[1]) out.add(m[1]);
        }

        const mLogfmt = n.match(/\b(?:msg|message)\s*=\s*"([^"]+)"/);
        if (mLogfmt?.[1]) out.add(mLogfmt[1]);

        const mGoQuoted = n.match(/]\s*"([^"]+)"/);
        if (mGoQuoted?.[1]) out.add(mGoQuoted[1]);

        const quoted = Array.from(n.matchAll(/"([^"]{10,})"/g)).map((m) => m[1]);
        for (const q of quoted) out.add(q);

        if (!isHeaderLikeLine(n)) {
            const tokens = Array.from(n.matchAll(/[A-Za-z0-9][A-Za-z0-9._-]{8,}/g)).map((m) => m[0]);
            for (const t of tokens) out.add(t);
        }

        return Array.from(out);
    };

    const buildCandidateKeywords = (uiText: string): string[] => {
        const lines = (uiText ?? "")
            .split(/\r?\n/)
            .map((l) => l.trim())
            .filter(Boolean);
        const fragments: string[] = [];
        for (const l of lines) fragments.push(...extractMsgLikeFragments(l));
        if (lines.length <= 1) fragments.push(...extractMsgLikeFragments(uiText));
        const normalized = fragments
            .map((s) => normalizeSpaces(String(s)))
            .filter((s) => s.length >= 8)
            .map((s) => (s.length > 80 ? s.slice(0, 80) : s));
        const unique = Array.from(new Set(normalized));
        if (unique.length > 0) return unique;
        const meaningfulLines = lines.filter((l) => !isHeaderLikeLine(l) && l.length >= 8);
        const baseLines = meaningfulLines.length > 0 ? meaningfulLines : lines.filter((l) => l.length >= 8);
        const fallbackLines = [
            baseLines[0],
            baseLines[baseLines.length - 1],
            baseLines.find((l) => l.length >= 20),
        ].filter(Boolean) as string[];
        const fallbackTokens: string[] = [];
        for (const l of fallbackLines) {
            const s1 = normalizeSpaces(l).slice(0, 120);
            const s2 = normalizeSpaces(l).slice(-120);
            if (s1.length >= 8) fallbackTokens.push(s1);
            if (s2.length >= 8 && s2 !== s1) fallbackTokens.push(s2);
        }
        return Array.from(new Set(fallbackTokens));
    };

    // 轮询等待 UI 日志真正加载出“可匹配片段”
    let uiLogText = "";
    let candidateKeywords: string[] = [];
    const maxUiRetry = 6;
    for (let i = 0; i < maxUiRetry; i++) {
        uiLogText = await getUiLogText(page);
        if (normalizeSpaces(uiLogText).length < 20) {
            await new Promise((r) => setTimeout(r, 1500));
            continue;
        }
        const uiLines = (uiLogText ?? "")
            .split(/\r?\n/)
            .map((l) => l.trim())
            .filter(Boolean);
        const meaningfulLines = uiLines.filter((l) => !isHeaderLikeLine(l) && l.length >= 8);
        // 只有 Pod/容器标题行时,继续等
        if (meaningfulLines.length === 0) {
            await new Promise((r) => setTimeout(r, 1500));
            continue;
        }
        candidateKeywords = buildCandidateKeywords(uiLogText);
        if (candidateKeywords.length > 0) break;
        await new Promise((r) => setTimeout(r, 1500));
    }
    expect(normalizeSpaces(uiLogText).length, "UI 日志为空或仍在显示暂无日志").toBeGreaterThanOrEqual(20);
    expect(candidateKeywords.length, "UI 日志中未提取到可用于匹配的关键片段").toBeGreaterThanOrEqual(1);
    const tryCommands: string[] = [];
    if (params.kubectlContainerName) {
        tryCommands.push(
            `kubectl logs -n ${namespace} ${podName} -c ${params.kubectlContainerName} --tail=500`,
        );
    }
    tryCommands.push(`kubectl logs -n ${namespace} ${podName} --tail=500`);
    tryCommands.push(`kubectl logs -n ${namespace} ${podName} --all-containers --tail=500`);

    let kubectlLogs = "";
    const maxRetry = 8;
    const intervalMs = 2000;
    for (let i = 0; i < maxRetry; i++) {
        for (const cmd of tryCommands) {
            const { stdout, code } = await execKubectl(ssh, cmd);
            if (code === 0 && stdout.trim().length > 0) {
                kubectlLogs = stdout;
                break;
            }
        }
        if (kubectlLogs.trim().length > 0) break;
        await new Promise((r) => setTimeout(r, intervalMs));
    }

    expect(kubectlLogs.trim().length, "kubectl 获取日志失败或日志为空").toBeGreaterThanOrEqual(20);
    const kubectlLogsNormalized = normalizeSpaces(kubectlLogs);
    const containsAny = candidateKeywords.some((kw) => kubectlLogsNormalized.includes(kw));
    expect(
        containsAny,
        `UI日志片段无法在 kubectl 日志中找到。\n` +
            `候选匹配关键字=${candidateKeywords.join(" | ")}\n` +
            `UI日志(前400)=${normalizeSpaces(uiLogText).slice(0, 400)}\n` +
            `kubectl日志(前400)=${kubectlLogsNormalized.slice(0, 400)}`,
    ).toBeTruthy();
}

export{
    goToAppMarket,
    installComponent,
    checkComponent,
    changeString,
    changePullPolicyLineWithWait,
    uninstallComponent,
    uninstallHelmComponent,
    selectLog,
    upgradeComponent,
    revertComponent,
    checkPrefixResourceNum,
    gotoApplicationResourceTab,
    clickSortHeader,
    getColumnTexts,
    expectSortedAsc,
    expectSortedDesc,
    expectDefaultSortState,
    verifyResourceSortInResourceTab,
    verifyOverviewSort,
    withKubectlSsh,
    kubectlGetFirstPodByNamePrefix,
    verifyUiLogWithKubectl,
    verifyLogExport,
    openYamlQuickDeploy,
    inputYamlContent,
    deleteDeploymentByName,
    getHelloWorldPackagePath,
    prepareHelloWorldPackageLikeMarketplace,
};