import { test } from '@affine-test/kit/playwright';
import {
  createRandomAIUser,
  loginUser,
  loginUserDirectly,
} from '@affine-test/kit/utils/cloud';
import { openHomePage, setCoreUrl } from '@affine-test/kit/utils/load-page';
import {
  clickNewPageButton,
  getBlockSuiteEditorTitle,
  waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar';
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
import { expect, type Page } from '@playwright/test';

test.describe.configure({ mode: 'parallel' });

const ONE_SECOND = 1000;
const TEN_SECONDS = 10 * ONE_SECOND;
const ONE_MINUTE = 60 * ONE_SECOND;

let isProduction = process.env.NODE_ENV === 'production';
if (
  process.env.PLAYWRIGHT_USER_AGENT &&
  process.env.PLAYWRIGHT_EMAIL &&
  !process.env.PLAYWRIGHT_PASSWORD
) {
  test.use({
    userAgent: process.env.PLAYWRIGHT_USER_AGENT || 'affine-tester',
  });
  setCoreUrl(process.env.PLAYWRIGHT_CORE_URL || 'http://localhost:8080');
  isProduction = true;
}

function getUser() {
  if (
    !isProduction ||
    !process.env.PLAYWRIGHT_EMAIL ||
    !process.env.PLAYWRIGHT_PASSWORD
  ) {
    return createRandomAIUser();
  }

  return {
    email: process.env.PLAYWRIGHT_EMAIL,
    password: process.env.PLAYWRIGHT_PASSWORD,
  };
}

test.skip(
  () =>
    !process.env.COPILOT_OPENAI_API_KEY ||
    !process.env.COPILOT_FAL_API_KEY ||
    process.env.COPILOT_OPENAI_API_KEY === '1' ||
    process.env.COPILOT_FAL_API_KEY === '1',
  'skip test if no copilot api key'
);

test('can open chat side panel', async ({ page }) => {
  await openHomePage(page);
  await waitForEditorLoad(page);
  await clickNewPageButton(page);
  await page.getByTestId('right-sidebar-toggle').click({
    delay: 200,
  });
  await page.getByTestId('sidebar-tab-chat').click();
  await expect(page.getByTestId('sidebar-tab-content-chat')).toBeVisible();
});

const makeChat = async (page: Page, content: string) => {
  if (await page.getByTestId('sidebar-tab-chat').isHidden()) {
    await page.getByTestId('right-sidebar-toggle').click({
      delay: 200,
    });
  }
  await page.getByTestId('sidebar-tab-chat').click();
  await page.getByTestId('chat-panel-input').focus();
  await page.keyboard.type(content);
  await page.keyboard.press('Enter');
};

const clearChat = async (page: Page) => {
  await page.getByTestId('chat-panel-clear').click();
  await page.getByTestId('confirm-modal-confirm').click();
};

const collectChat = async (page: Page) => {
  const chatPanel = await page.waitForSelector('.chat-panel-messages');
  if (await chatPanel.$('.chat-panel-messages-placeholder')) {
    return [];
  }
  // wait ai response
  await page.waitForSelector('.chat-panel-messages .message chat-copy-more');
  const lastMessage = await chatPanel.$$('.message').then(m => m[m.length - 1]);
  await lastMessage.waitForSelector('chat-copy-more');
  await page.waitForTimeout(ONE_SECOND);
  return Promise.all(
    Array.from(await chatPanel.$$('.message')).map(async m => ({
      name: await m.$('.user-info').then(i => i?.innerText()),
      content: await m
        .$('chat-text')
        .then(t => t?.$('editor-host'))
        .then(e => e?.innerText()),
    }))
  );
};

const focusToEditor = async (page: Page) => {
  const title = getBlockSuiteEditorTitle(page);
  await title.focus();
  await page.keyboard.press('Enter');
};

const getEditorContent = async (page: Page) => {
  let content = '';
  let retry = 3;
  while (!content && retry > 0) {
    const lines = await page.$$('page-editor .inline-editor');
    const contents = await Promise.all(lines.map(el => el.innerText()));
    content = contents
      // cleanup zero width space
      .map(c => c.replace(/\u200B/g, '').trim())
      .filter(c => !!c)
      .join('\n');
    if (!content) {
      await page.waitForTimeout(500);
      retry -= 1;
    }
  }
  return content;
};

const switchToEdgelessMode = async (page: Page) => {
  const editor = await page.waitForSelector('page-editor');
  await page.getByTestId('switch-edgeless-mode-button').click();
  // wait for new editor
  editor.waitForElementState('hidden');
  await page.waitForSelector('edgeless-editor');
};

test('can trigger login at chat side panel', async ({ page }) => {
  await openHomePage(page);
  await waitForEditorLoad(page);
  await clickNewPageButton(page);
  await makeChat(page, 'hello');
  const loginTips = await page.waitForSelector('ai-error-wrapper');
  expect(await loginTips.innerText()).toBe('Login');
});

test('can chat after login at chat side panel', async ({ page }) => {
  await openHomePage(page);
  await waitForEditorLoad(page);
  await clickNewPageButton(page);
  await makeChat(page, 'hello');
  const loginTips = await page.waitForSelector('ai-error-wrapper');
  (await loginTips.$('div'))!.click();
  // login
  const user = await getUser();
  await loginUserDirectly(page, user);
  // after login
  await makeChat(page, 'hello');
  const history = await collectChat(page);
  expect(history[0]).toEqual({ name: 'You', content: 'hello' });
  expect(history[1].name).toBe('AFFiNE AI');
});

test.describe('chat panel', () => {
  let user: {
    email: string;
    password: string;
  };

  test.beforeEach(async ({ page }) => {
    user = await getUser();
    await loginUser(page, user);
  });

  test('basic chat', async ({ page }) => {
    await page.reload();
    await clickSideBarAllPageButton(page);
    await page.waitForTimeout(200);
    await createLocalWorkspace({ name: 'test' }, page);
    await clickNewPageButton(page);
    await makeChat(page, 'hello');
    const history = await collectChat(page);
    expect(history[0]).toEqual({ name: 'You', content: 'hello' });
    expect(history[1].name).toBe('AFFiNE AI');
    await clearChat(page);
    expect((await collectChat(page)).length).toBe(0);
  });

  test('chat actions', async ({ page }) => {
    await page.reload();
    await clickSideBarAllPageButton(page);
    await page.waitForTimeout(200);
    await createLocalWorkspace({ name: 'test' }, page);
    await clickNewPageButton(page);
    await makeChat(page, 'hello');
    const content = (await collectChat(page))[1].content;
    await page.getByTestId('action-copy-button').click();
    await page.waitForTimeout(500);
    expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(
      content
    );
    await page.getByTestId('action-retry-button').click();
    expect((await collectChat(page))[1].content).not.toBe(content);
  });

  test('can be insert below', async ({ page }) => {
    await page.reload();
    await clickSideBarAllPageButton(page);
    await page.waitForTimeout(200);
    await createLocalWorkspace({ name: 'test' }, page);
    await clickNewPageButton(page);
    await makeChat(page, 'hello');
    const content = (await collectChat(page))[1].content;
    await focusToEditor(page);
    // insert below
    await page.getByTestId('action-insert-below').click();
    await page.waitForSelector('affine-format-bar-widget editor-toolbar');
    const editorContent = await getEditorContent(page);
    expect(editorContent).toBe(content);
  });

  test('can be add to edgeless as node', async ({ page }) => {
    await page.reload();
    await clickSideBarAllPageButton(page);
    await page.waitForTimeout(200);
    await createLocalWorkspace({ name: 'test' }, page);
    await clickNewPageButton(page);
    await makeChat(page, 'hello');
    const content = (await collectChat(page))[1].content;
    await switchToEdgelessMode(page);
    // delete default note
    await (await page.waitForSelector('affine-edgeless-note')).click();
    page.keyboard.press('Delete');
    // insert note
    await page.getByTestId('action-add-to-edgeless-as-note').click();
    const edgelessNode = await page.waitForSelector('affine-edgeless-note');
    expect(await edgelessNode.innerText()).toBe(content);
  });

  test('can be create as a doc', async ({ page }) => {
    await page.reload();
    await clickSideBarAllPageButton(page);
    await page.waitForTimeout(200);
    await createLocalWorkspace({ name: 'test' }, page);
    await clickNewPageButton(page);
    await makeChat(page, 'hello');
    const content = (await collectChat(page))[1].content;
    const editor = await page.waitForSelector('page-editor');
    await page.getByTestId('action-create-as-a-doc').click();
    // wait for new editor
    editor.waitForElementState('hidden');
    await page.waitForSelector('page-editor');
    const editorContent = await getEditorContent(page);
    expect(editorContent).toBe(content);
  });

  // feature not launched yet
  test.skip('can be save chat to block', async ({ page }) => {
    await page.reload();
    await clickSideBarAllPageButton(page);
    await page.waitForTimeout(200);
    await createLocalWorkspace({ name: 'test' }, page);
    await clickNewPageButton(page);
    await makeChat(page, 'hello');
    const contents = (await collectChat(page)).map(m => m.content);
    await switchToEdgelessMode(page);
    await page.getByTestId('action-save-chat-to-block').click();
    const chatBlock = await page.waitForSelector('affine-edgeless-ai-chat');
    expect(
      await Promise.all(
        (await chatBlock.$$('.ai-chat-user-message')).map(m => m.innerText())
      )
    ).toBe(contents);
  });

  test('can be chat and insert below in page mode', async ({ page }) => {
    await page.reload();
    await clickSideBarAllPageButton(page);
    await page.waitForTimeout(200);
    await createLocalWorkspace({ name: 'test' }, page);
    await clickNewPageButton(page);
    await focusToEditor(page);
    await page.keyboard.type('/');
    await page.getByTestId('sub-menu-0').getByText('Ask AI').click();
    const input = await page.waitForSelector('ai-panel-input textarea');
    await input.fill('hello');
    await input.press('Enter');
    const resp = await page.waitForSelector(
      'ai-panel-answer .response-list-container'
    ); // wait response
    const content = await (
      await page.waitForSelector('ai-panel-answer editor-host')
    ).innerText();
    await (await resp.waitForSelector('.ai-item-insert-below')).click();
    const editorContent = await getEditorContent(page);
    expect(editorContent).toBe(content);
  });

  test('can be retry or discard chat in page mode', async ({ page }) => {
    await page.reload();
    await clickSideBarAllPageButton(page);
    await page.waitForTimeout(200);
    await createLocalWorkspace({ name: 'test' }, page);
    await clickNewPageButton(page);
    await focusToEditor(page);
    await page.keyboard.type('/');
    await page.getByTestId('sub-menu-0').getByText('Ask AI').click();
    const input = await page.waitForSelector('ai-panel-input textarea');
    await input.fill('hello');
    await input.press('Enter');
    // regenerate
    {
      const resp = await page.waitForSelector(
        'ai-panel-answer .response-list-container:last-child'
      );
      const answerEditor = await page.waitForSelector(
        'ai-panel-answer editor-host'
      );
      const content = await answerEditor.innerText();
      await (await resp.waitForSelector('.ai-item-regenerate')).click();
      await page.waitForSelector('ai-panel-answer .response-list-container'); // wait response
      expect(
        await (
          await (
            await page.waitForSelector('ai-panel-answer')
          ).waitForSelector('editor-host')
        ).innerText()
      ).not.toBe(content);
    }

    // discard
    {
      const resp = await page.waitForSelector(
        'ai-panel-answer .response-list-container:last-child'
      );
      await (await resp.waitForSelector('.ai-item-discard')).click();
      await page.getByTestId('confirm-modal-confirm').click();
      const editorContent = await getEditorContent(page);
      expect(editorContent).toBe('');
    }
  });
});

test.describe('chat with block', () => {
  let user: {
    email: string;
    password: string;
  };

  test.beforeEach(async ({ page }) => {
    user = await getUser();
    await loginUser(page, user);
  });

  const collectTextAnswer = async (page: Page) => {
    // wait ai response
    await page.waitForSelector(
      'affine-ai-panel-widget .response-list-container',
      { timeout: ONE_MINUTE }
    );
    const answer = await page.waitForSelector(
      'affine-ai-panel-widget ai-panel-answer editor-host'
    );
    return answer.innerText();
  };

  const collectImageAnswer = async (page: Page, timeout = TEN_SECONDS) => {
    // wait ai response
    await page.waitForSelector(
      'affine-ai-panel-widget .response-list-container',
      { timeout }
    );
    const answer = await page.waitForSelector(
      'affine-ai-panel-widget .ai-answer-image img'
    );
    return answer.getAttribute('src');
  };

  const disableEditorBlank = async (page: Page) => {
    // hide blank element, this may block the click
    const blank = page.getByTestId('page-editor-blank');
    if (await blank.isVisible()) {
      await blank.evaluate(node => (node.style.pointerEvents = 'none'));
    } else {
      console.warn('blank element not found');
    }
  };

  test.describe('chat with text', () => {
    const pasteTextToPageEditor = async (page: Page, content: string) => {
      await focusToEditor(page);
      await page.keyboard.type(content);
    };

    test.beforeEach(async ({ page }) => {
      await page.reload();
      await clickSideBarAllPageButton(page);
      await page.waitForTimeout(200);
      await createLocalWorkspace({ name: 'test' }, page);
      await clickNewPageButton(page);
      await pasteTextToPageEditor(page, 'hello');
    });

    test.beforeEach(async ({ page }) => {
      await page.waitForSelector('affine-paragraph').then(i => i.click());
      await page.keyboard.press('ControlOrMeta+A');
      await page
        .waitForSelector('page-editor editor-toolbar ask-ai-button')
        .then(b => b.click());
    });

    const options = [
      // review with ai
      'Fix spelling',
      'Fix Grammar',
      // edit with ai
      'Explain selection',
      'Improve writing',
      'Make it longer',
      'Make it shorter',
      'Continue writing',
      // generate with ai
      'Summarize',
      'Generate headings',
      'Generate outline',
      'Find actions',
      // draft with ai
      'Write an article about this',
      'Write a tweet about this',
      'Write a poem about this',
      'Write a blog post about this',
      'Brainstorm ideas about this',
    ];
    for (const option of options) {
      test(option, async ({ page }) => {
        await disableEditorBlank(page);
        await page
          .waitForSelector(
            `.ai-item-${option.replaceAll(' ', '-').toLowerCase()}`
          )
          .then(i => i.click());
        expect(await collectTextAnswer(page)).toBeTruthy();
      });
    }
  });

  test.describe('chat with image block', () => {
    const pasteImageToPageEditor = async (page: Page) => {
      await page.evaluate(async () => {
        const canvas = document.createElement('canvas');
        canvas.width = 100;
        canvas.height = 100;
        const blob = await new Promise<Blob>(resolve =>
          canvas.toBlob(blob => resolve(blob!), 'image/png')
        );
        await navigator.clipboard.write([
          new ClipboardItem({ [blob.type]: blob }),
        ]);
      });
      await focusToEditor(page);
      await page.keyboard.press('ControlOrMeta+V');
    };

    test.beforeEach(async ({ page }) => {
      await page.reload();
      await clickSideBarAllPageButton(page);
      await page.waitForTimeout(200);
      await createLocalWorkspace({ name: 'test' }, page);
      await clickNewPageButton(page);
      await pasteImageToPageEditor(page);
    });

    test.describe('page mode', () => {
      test.beforeEach(async ({ page }) => {
        await disableEditorBlank(page);
        await page.waitForSelector('affine-image').then(i => i.click());
        await page
          .waitForSelector('affine-image editor-toolbar ask-ai-button')
          .then(b => b.click());
      });

      test('explain this image', async ({ page }) => {
        await page
          .waitForSelector('.ai-item-explain-this-image')
          .then(i => i.click());
        expect(await collectTextAnswer(page)).toBeTruthy();
      });

      test('generate a caption', async ({ page }) => {
        await page
          .waitForSelector('.ai-item-generate-a-caption')
          .then(i => i.click());
        expect(await collectTextAnswer(page)).toBeTruthy();
      });

      test('continue with ai', async ({ page }) => {
        await page
          .waitForSelector('.ai-item-continue-with-ai')
          .then(i => i.click());
        await page
          .waitForSelector('chat-panel-input .chat-panel-images')
          .then(el => el.waitForElementState('visible'));
      });

      test('open ai chat', async ({ page }) => {
        await page
          .waitForSelector('.ai-item-open-ai-chat')
          .then(i => i.click());
        const cards = await page.waitForSelector('chat-panel chat-cards');
        await cards.waitForElementState('visible');
        const cardTitles = await Promise.all(
          await cards
            .$$('.card-wrapper .card-title')
            .then(els => els.map(async el => await el.innerText()))
        );
        expect(cardTitles).toContain('Start with this Image');
      });
    });

    // TODO(@darkskygit): block by BS-1709, enable this after bug fix
    test.describe.skip('edgeless mode', () => {
      test.beforeEach(async ({ page }) => {
        await switchToEdgelessMode(page);
        const note = await page.waitForSelector('affine-edgeless-note');

        {
          // move note to avoid menu overlap
          const box = (await note.boundingBox())!;
          page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
          page.mouse.down();
          // sleep to avoid flicker
          await page.waitForTimeout(500);
          page.mouse.move(box.x + box.width / 2, box.y + box.height / 2 - 200);
          await page.waitForTimeout(500);
          page.mouse.up();
          note.click();
        }

        await disableEditorBlank(page);
        await page.waitForSelector('affine-image').then(i => i.click());
        await page
          .waitForSelector('affine-image editor-toolbar ask-ai-button')
          .then(b => b.click());
      });

      // skip by default, dalle is very slow
      test.skip('generate an image', async ({ page }) => {
        await page
          .waitForSelector('.ai-item-generate-an-image')
          .then(i => i.click());
        await page.keyboard.type('a cat');
        await page.keyboard.press('Enter');
        expect(await collectImageAnswer(page)).toBeTruthy();
      });

      const processes = [
        'Clearer',
        'Remove background',
        // skip by default, need a face in image
        // 'Convert to sticker',
      ];
      for (const process of processes) {
        test(`image processing ${process}`, async ({ page }) => {
          await page
            .waitForSelector('.ai-item-image-processing')
            .then(i => i.hover());
          await page.getByText(process).click();
          {
            // to be remove
            await page.keyboard.type(',');
            await page.keyboard.press('Enter');
          }
          expect(await collectImageAnswer(page, ONE_MINUTE * 2)).toBeTruthy();
        });
      }

      const filters = ['Clay', 'Sketch', 'Anime', 'Pixel'];
      for (const filter of filters) {
        test(`ai image ${filter.toLowerCase()} filter`, async ({ page }) => {
          await page
            .waitForSelector('.ai-item-ai-image-filter')
            .then(i => i.hover());
          await page.getByText(`${filter} style`).click();
          {
            // to be remove
            await page.keyboard.type(',');
            await page.keyboard.press('Enter');
          }
          expect(await collectImageAnswer(page, ONE_MINUTE * 2)).toBeTruthy();
        });
      }
    });
  });
});