/**
 * Test-scoped fixtures added by playwright-bdd.
 */

/* eslint-disable @typescript-eslint/no-unused-vars */

import { test as base } from './bddWorkerFixtures';
import { getScenarioHooksToRun, runScenarioHooks } from '../hooks/scenario';
import { BddStepInvoker, BddStepFn } from './bddStepInvoker';
import { TestTypeCommon } from '../playwright/types';
import { TestInfo } from '@playwright/test';
import { BddTestData } from '../bddData/types';
import { BddContext, BddStepInfo } from './bddContext';
import { PromptFixture } from '../ai/promptAttachment';

// BDD fixtures prefixed with '$' to avoid collision with user's fixtures.

// Hide all BDD fixtures in reporter.
// 'box' option was added in PW 1.46,
// make type coercion to satisfy TS in early PW versions
const fixtureOptions = { scope: 'test', box: true } as { scope: 'test' };

export type BddTestFixtures = {
  $bddContext: BddContext;
  Given: BddStepFn;
  When: BddStepFn;
  Then: BddStepFn;
  And: BddStepFn;
  But: BddStepFn;
  $bddFileData: BddTestData[];
  $bddTestData?: BddTestData; // $bddTestData is undefined for non-bdd tests
  $tags: string[];
  $test: TestTypeCommon;
  $testInfo: TestInfo;
  $step: BddStepInfo;
  $uri: string;
  $applySpecialTags: void;
  $world: unknown;
  $beforeEachFixtures: Record<string, unknown>;
  $beforeEach: void;
  $afterEachFixtures: Record<string, unknown>;
  $afterEach: void;
  $prompt: PromptFixture;
};

export const test = base.extend<BddTestFixtures>({
  // apply timeout and slow from special tags in runtime instead of generating in test body
  // to have cleaner test body and track fixtures in timeout calculation.
  $applySpecialTags: [
    async ({ $bddTestData }, use, testInfo) => {
      if ($bddTestData?.timeout !== undefined) testInfo.setTimeout($bddTestData.timeout);
      if ($bddTestData?.slow) testInfo.slow();
      await use();
    },
    fixtureOptions,
  ],
  $bddContext: [
    async ({ $tags, $test, $bddConfig, $bddTestData, $uri, $step }, use, testInfo) => {
      if (!$bddTestData) throw errorBddTestDataNotFound(testInfo);

      await use({
        config: $bddConfig,
        featureUri: $uri,
        testInfo,
        test: $test,
        tags: $tags,
        step: $step,
        stepIndex: -1,
        bddTestData: $bddTestData,
      });
    },
    fixtureOptions,
  ],

  // Important to pass $world dependency here, not via $bddContext
  // to avoid circular dependency, see #319.
  //
  // Unused fixtures here are important:
  // - $applySpecialTags: to apply special tags before test run
  // - $beforeEach: to not run any steps in background's test.beforeEach
  //   because Playwright runs all before* hooks even in case of error in one of them
  //   See: https://github.com/microsoft/playwright/issues/28285
  Given: [
    async ({ $bddContext, $world, $applySpecialTags, $beforeEach }, use) => {
      const invoker = new BddStepInvoker($bddContext, $world);
      await use(invoker.invoke.bind(invoker));
    },
    fixtureOptions,
  ],
  // All invoke step fixtures use the same Given fixture, b/c we get keyword from bddStepData
  When: [({ Given }, use) => use(Given), fixtureOptions],
  Then: [({ Given }, use) => use(Given), fixtureOptions],
  And: [({ Given }, use) => use(Given), fixtureOptions],
  But: [({ Given }, use) => use(Given), fixtureOptions],

  // For cucumber-style $world will be overwritten in test files
  // For playwright-style $world will be empty object
  // Note: although pw-style does not expect usage of world in steps,
  // some projects request it for better migration process from cucumber
  // See: https://github.com/vitalets/playwright-bdd/issues/208
  $world: [({}, use: (arg: unknown) => unknown) => use({}), fixtureOptions],

  // init $bddFileData with empty array, will be overwritten in each BDD test file
  $bddFileData: [({}, use) => use([]), fixtureOptions],

  // bddTestData for particular test
  $bddTestData: [
    async ({ $bddFileData }, use, testInfo) => {
      const bddTestData = $bddFileData.find((data) => data.pwTestLine === testInfo.line);
      await use(bddTestData);
    },
    fixtureOptions,
  ],

  // particular test tags
  $tags: [({ $bddTestData }, use) => use($bddTestData?.tags || []), fixtureOptions],

  // init $test with base test, but it will be overwritten in test file
  $test: [({}, use) => use(base), fixtureOptions],

  $testInfo: [({}, use, testInfo) => use(testInfo), fixtureOptions],

  // Info of the currently executed step.
  // Filled dynamically in step invoker.
  // Important to keep this fixture separate, without dependency on bddContext.
  // Otherwise we can get cyclic fixtures dependency.
  $step: [({}, use) => use({ title: '' }), fixtureOptions],

  // feature file uri, relative to configDir, will be overwritten in test file
  $uri: [({}, use) => use(''), fixtureOptions],

  // can be overwritten in test file if there are fixtures for beforeEach hooks
  $beforeEachFixtures: [({}, use) => use({}), fixtureOptions],
  // can be overwritten in test file if there are fixtures for afterEach hooks
  $afterEachFixtures: [({}, use) => use({}), fixtureOptions],
  // runs beforeEach hooks
  $beforeEach: [
    async ({ $bddContext, $world, $beforeEachFixtures }, use) => {
      const hooksToRun = getScenarioHooksToRun('before', $bddContext.tags);
      await runScenarioHooks(hooksToRun, $world, { $bddContext, ...$beforeEachFixtures });
      await use();
    },
    fixtureOptions,
  ],
  // runs afterEach hooks
  $afterEach: [
    async ({ $bddContext, $world, $afterEachFixtures }, use) => {
      await use();
      const hooksToRun = getScenarioHooksToRun('after', $bddContext.tags);
      hooksToRun.reverse();
      await runScenarioHooks(hooksToRun, $world, { $bddContext, ...$afterEachFixtures });
    },
    fixtureOptions,
  ],
  $prompt: [
    async ({ $bddContext }, use) => {
      const prompt = new PromptFixture($bddContext);
      await use(prompt);
      await prompt.attach();
    },
    fixtureOptions,
  ],
});

function errorBddTestDataNotFound(testInfo: TestInfo) {
  const testLocation = testInfo.file + ':' + testInfo.line;
  return new Error(`bddTestData not found for test: ${testLocation}`);
}
