// Copyright 2017-2026 @pezkuwi/app-bounties authors & contributors
// SPDX-License-Identifier: Apache-2.0

/* global jest, expect */

import type { RenderResult } from '@testing-library/react';
import type { ApiPromise } from '@pezkuwi/api';
import type { DeriveCollectiveProposal } from '@pezkuwi/api-derive/types';
import type { ApiProps } from '@pezkuwi/react-api/types';
import type { PartialQueueTxExtrinsic, QueueProps, QueueTxExtrinsicAdd } from '@pezkuwi/react-components/Status/types';
import type { BountyIndex } from '@pezkuwi/types/interfaces';
import type { PezpalletBountiesBounty, PezpalletBountiesBountyStatus } from '@pezkuwi/types/lookup';
import type { BountyApi } from '../../src/hooks/index.js';

import { fireEvent, render, within } from '@testing-library/react';
import React, { Suspense } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { ThemeProvider } from 'styled-components';

import { PEZKUWI_GENESIS } from '@pezkuwi/apps-config';
import { lightTheme } from '@pezkuwi/react-components';
import { KeyringCtxRoot } from '@pezkuwi/react-hooks';
import { ApiCtx } from '@pezkuwi/react-hooks/ctx/Api';
import { QueueCtx } from '@pezkuwi/react-hooks/ctx/Queue';
import { balanceOf } from '@pezkuwi/test-support/creation/balance';
import { BountyFactory } from '@pezkuwi/test-support/creation/bounties';
import { TypeRegistry } from '@pezkuwi/types/create';

import Bounties from '../../src/Bounties.js';
import { mockBountyHooks } from '../hooks/defaults.js';
import { clickButtonWithName } from '../utils/clickButtonWithName.js';
import { clickElementWithTestId } from '../utils/clickElementWithTestId.js';
import { clickElementWithText } from '../utils/clickElementWithText.js';

function aGenesisHash () {
  return new TypeRegistry().createType('Hash', PEZKUWI_GENESIS);
}

type FindOne = (match: string) => Promise<HTMLElement>;
type FindManyWithMatcher = (match: string | ((match: string) => boolean)) => Promise<HTMLElement[]>
type GetMany = (match: string) => HTMLElement[];

class NotYetRendered extends Error {

}

let queueExtrinsic: (value: PartialQueueTxExtrinsic) => void;
const propose = jest.fn(() => 'mockProposeExtrinsic');

interface RenderedBountiesPage {
  findAllByTestId: FindManyWithMatcher;
  findByText: FindOne;
  findByRole: FindOne;
  findByTestId: FindOne;
  getAllByRole: GetMany;
  queryAllByText: GetMany;
}

export class BountiesPage {
  aBounty: ({ status, value }?: Partial<PezpalletBountiesBounty>) => PezpalletBountiesBounty;
  aBountyIndex: (index?: number) => BountyIndex;
  aBountyStatus: (status: string) => PezpalletBountiesBountyStatus;
  bountyStatusWith: ({ curator, status }: { curator?: string, status?: string, }) => PezpalletBountiesBountyStatus;
  bountyWith: ({ status, value }: { status?: string, value?: number }) => PezpalletBountiesBounty;

  findByRole?: FindOne;
  findByText?: FindOne;
  findByTestId?: FindOne;
  getAllByRole?: GetMany;
  findAllByTestId?: FindManyWithMatcher;
  queryAllByText?: GetMany;
  renderResult?: RenderResult;

  constructor (api: ApiPromise) {
    ({ aBounty: this.aBounty, aBountyIndex: this.aBountyIndex, aBountyStatus: this.aBountyStatus, bountyStatusWith: this.bountyStatusWith, bountyWith: this.bountyWith } = new BountyFactory(api as any));
  }

  renderOne (bounty: PezpalletBountiesBounty, proposals: DeriveCollectiveProposal[] = [], description = '', index = this.aBountyIndex()): RenderedBountiesPage {
    return this.renderMany({ bounties: [{ bounty, description, index, proposals }] });
  }

  renderMany (bountyApi: Partial<BountyApi> = {}, { balance = 1 } = {}): RenderedBountiesPage {
    const renderResult = this.renderBounties(bountyApi, { balance });
    const { findAllByTestId, findByRole, findByTestId, findByText, getAllByRole, queryAllByText } = renderResult;

    this.findByRole = findByRole;
    this.findByText = findByText;
    this.findByTestId = findByTestId;
    this.getAllByRole = getAllByRole;
    this.findAllByTestId = findAllByTestId;
    this.queryAllByText = queryAllByText;
    this.renderResult = renderResult;

    return { findAllByTestId, findByRole, findByTestId, findByText, getAllByRole, queryAllByText };
  }

  private renderBounties (bountyApi: Partial<BountyApi> = {}, { balance = 1 } = {}) {
    mockBountyHooks.bountyApi = { ...mockBountyHooks.bountyApi, ...bountyApi };
    mockBountyHooks.balance = balanceOf(balance);
    const mockApi: ApiProps = {
      api: {
        derive: {
          accounts: {
            info: () => Promise.resolve(() => { /**/
            })
          }
        },
        genesisHash: aGenesisHash(),
        query: {},
        registry: { chainDecimals: [12], chainTokens: ['Unit'] },
        tx: {
          council: {
            propose
          }
        }
      },
      isApiConnected: true,
      isApiInitialized: true,
      isApiReady: true,
      isEthereum: false,
      systemName: 'bizinikiwi'
    } as unknown as ApiProps;

    queueExtrinsic = jest.fn() as QueueTxExtrinsicAdd;
    const queue = {
      queueExtrinsic
    } as QueueProps;

    return render(
      <>
        <div id='tooltips' />
        <Suspense fallback='...'>
          <QueueCtx.Provider value={queue}>
            <MemoryRouter>
              <ThemeProvider theme={lightTheme}>
                <ApiCtx.Provider value={mockApi}>
                  <KeyringCtxRoot>
                    <Bounties />
                  </KeyringCtxRoot>
                </ApiCtx.Provider>
              </ThemeProvider>
            </MemoryRouter>
          </QueueCtx.Provider>
        </Suspense>
      </>
    );
  }

  private assertRendered (): asserts this is RenderedBountiesPage {
    if (this.findByText === undefined) {
      throw new NotYetRendered();
    }
  }

  async openProposeCurator (): Promise<void> {
    this.assertRendered();
    const proposeCuratorButton = await this.findByText('Propose curator');

    fireEvent.click(proposeCuratorButton);
    // await this.expectText('This action will create a Council motion to propose a Curator for the Bounty.');
  }

  async enterCuratorsFee (fee: string): Promise<void> {
    this.assertRendered();
    const feeInput = await this.findByTestId("curator's fee");

    fireEvent.change(feeInput, { target: { value: fee } });
  }

  async expectText (expected: string): Promise<void> {
    this.assertRendered();
    expect(await this.findByText(expected)).toBeTruthy();
  }

  async assignCuratorButton (): Promise<HTMLElement> {
    this.assertRendered();
    const proposeCuratorModal = await this.findByTestId('propose-curator-modal');

    return await within(proposeCuratorModal).findByText('Propose curator');
  }

  enterProposingAccount (account: string): void {
    this.assertRendered();
    const comboboxes = this.getAllByRole('combobox');

    const proposingAccountInput = comboboxes[0].children[0];

    fireEvent.change(proposingAccountInput, { target: { value: account } });
    fireEvent.keyDown(proposingAccountInput, { code: 'Enter', key: 'Enter' });
  }

  enterProposedCurator (curator: string): void {
    this.assertRendered();
    const comboboxes = this.getAllByRole('combobox');

    const proposedCuratorInput = comboboxes[1].children[0];

    fireEvent.change(proposedCuratorInput, { target: { value: curator } });
    fireEvent.keyDown(proposedCuratorInput, { code: 'Enter', key: 'Enter' });
  }

  expectExtrinsicQueued (extrinsicPart: { accountId: string; extrinsic?: string }): void {
    expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining(extrinsicPart));
  }

  expectTextAbsent (text: string): void {
    this.assertRendered();
    expect(this.queryAllByText(text)).toHaveLength(0);
  }

  async findAllDescriptions (): Promise<string[]> {
    this.assertRendered();
    const descriptions = await this.findAllByTestId('description');

    return descriptions.map((d) => d.textContent || '');
  }

  async rendered (): Promise<void> {
    this.assertRendered();
    await this.findByTestId('bountyStatus');
  }

  async openAddBounty (): Promise<void> {
    this.assertRendered();
    await clickButtonWithName('Add Bounty', this.findByRole);
    await this.expectText('This account will propose the bounty. Bond amount will be reserved on its balance.');
  }

  async enterBountyTitle (title: string): Promise<void> {
    this.assertRendered();
    const titleInput = await this.findByTestId('bounty title');

    fireEvent.change(titleInput, { target: { value: title } });
  }

  async openCloseBounty (): Promise<void> {
    this.assertRendered();
    await this.openExtraActions();

    await clickElementWithText('Close', this.findByText);

    // await this.expectText('This action will create a Council proposal to close the Bounty.');
  }

  async clickButton (buttonName: string): Promise<void> {
    this.assertRendered();
    await clickButtonWithName(buttonName, this.findByRole);
  }

  async clickButtonByTestId (buttonName: string): Promise<void> {
    this.assertRendered();
    await clickElementWithTestId(buttonName, this.findByTestId);
  }

  async clickButtonByText (buttonName: string): Promise<void> {
    this.assertRendered();
    await clickElementWithText(buttonName, this.findByText);
  }

  async openRejectCuratorRole (): Promise<void> {
    await this.openExtraActions();
    await this.clickButtonByText('Reject curator');
    // await this.expectText('This action will reject your candidacy for the curator of the bounty.');
  }

  async openExtraActions (): Promise<void> {
    await this.clickButtonByTestId('popup-open');
  }

  async openAcceptCuratorRole (): Promise<void> {
    await this.clickButton('Accept');
    // await this.expectText('This action will accept your candidacy for the curator of the bounty.');
  }

  async findCuratorsFee (): Promise<string> {
    this.assertRendered();

    return (await this.findByTestId("curator's fee")).getAttribute('value') || '';
  }

  async findCuratorsDeposit (): Promise<string> {
    this.assertRendered();

    return (await this.findByTestId("curator's deposit")).getAttribute('value') || '';
  }

  async openExtendExpiry (): Promise<void> {
    await this.openExtraActions();
    await this.clickButtonByText('Extend expiry');
    // await this.expectText('This action will extend expiry time of the selected bounty.');
  }

  async enterExpiryRemark (remark: string): Promise<void> {
    this.assertRendered();
    const remarkInput = await this.findByTestId('bounty remark');

    fireEvent.change(remarkInput, { target: { value: remark } });
  }

  async openGiveUpCuratorsRole (): Promise<void> {
    await this.openExtraActions();
    await this.clickButtonByText('Give up');
    // await this.expectText('This action will unassign you from the curator role.');
  }

  async openSlashCuratorByCouncil (): Promise<void> {
    await this.openExtraActions();
    await this.clickButtonByText('Slash curator (Council)');
    // await this.expectText('This action will create a Council motion to slash the Curator.');
  }

  async openAwardBeneficiary (): Promise<void> {
    await this.clickButton('Reward implementer');
    // await this.expectText('This action will reward the Beneficiary and close the bounty after a delay period.');
  }

  enterBeneficiary (beneficiary: string): void {
    this.assertRendered();
    const comboboxes = this.getAllByRole('combobox');

    const beneficiaryAccountInput = comboboxes[1].children[0];

    fireEvent.change(beneficiaryAccountInput, { target: { value: beneficiary } });
    fireEvent.keyDown(beneficiaryAccountInput, { code: 'Enter', key: 'Enter' });
  }

  async expectVotingDescription (description: string): Promise<void> {
    this.assertRendered();
    const votingInfo = await this.findByTestId('voting-description');
    const icon = await within(votingInfo).findByTestId('question-circle');

    fireEvent.mouseEnter(icon);
    expect(await this.findByText(description)).toBeVisible();
  }
}
