screenshot_opts_schema = [
  full_page: [type: :boolean, default: true],
  omit_background: [type: :boolean, default: false]
]

browsers = ~w(android chromium electron firefox webkit)a

playwright_recommended_version = "1.55.0"

schema =
  NimbleOptions.new!(
    browser: [
      default: :chromium,
      type: {:in, browsers},
      type_doc: "`#{Enum.map_join(browsers, " | ", &":#{&1}")}`"
    ],
    runner: [
      default: "npx",
      type_spec: quote(do: binary()),
      type_doc: "`t:binary/0`",
      type: {:custom, PhoenixTest.Playwright.Config, :__validate_runner__, []},
      doc:
        "The JS package runner to use to run the Playwright CLI. Accepts either a binary executable exposed in PATH or the absolute path to it."
    ],
    assets_dir: [
      default: "./assets",
      type_spec: quote(do: binary()),
      type_doc: "`t:binary/0`",
      type: {:custom, PhoenixTest.Playwright.Config, :__validate_assets_dir__, []},
      doc:
        "The directory where the JS assets are located and the Playwright CLI is installed.\n" <>
          "Playwright version `#{playwright_recommended_version}` or newer is recommended."
    ],
    cli: [
      type: {:custom, PhoenixTest.Playwright.Config, :__validate_cli__, []},
      deprecated: "Use `assets_dir` instead."
    ],
    executable_path: [
      type: :string,
      doc: "Path to a browser executable to run instead of the bundled one. Use at your own risk."
    ],
    headless: [
      default: true,
      type: :boolean
    ],
    js_logger: [
      default: :default,
      type: {:or, [{:in, [:default, false]}, {:fun, 1}]},
      type_doc: "`:default | false | (msg -> nil)`"
    ],
    screenshot: [
      default: false,
      type: {:or, [:boolean, non_empty_keyword_list: screenshot_opts_schema]},
      type_doc: "`boolean/0 | Keyword.t/0`",
      doc:
        "Either a boolean or a keyword list:\n\n" <>
          NimbleOptions.docs(screenshot_opts_schema, nest_level: 1)
    ],
    screenshot_dir: [
      default: "screenshots",
      type: :string
    ],
    browser_launch_timeout: [
      default: to_timeout(second: 4),
      type: :non_neg_integer
    ],
    timeout: [
      default: to_timeout(second: 2),
      type: :non_neg_integer
    ],
    slow_mo: [
      default: to_timeout(second: 0),
      type: :non_neg_integer
    ],
    trace: [
      default: false,
      type: {:in, [false, true, :open]},
      type_doc: "`boolean/0 | :open`"
    ],
    trace_dir: [
      default: "traces",
      type: :string
    ],
    accept_dialogs: [
      default: true,
      type: :boolean,
      doc: "Accept browser dialogs (`alert()`, `confirm()`, `prompt()`)"
    ],
    browser_context_opts: [
      default: [],
      type: {:or, [:map, :keyword_list]},
      doc:
        "Additional arguments passed to Playwright [Browser.newContext](https://playwright.dev/docs/api/class-browser#browser-new-context).\n" <>
          ~s(E.g. `[http_credentials: %{username: "a", password: "b"}]`)
    ],
    browser_page_opts: [
      default: [],
      type: {:or, [:map, :keyword_list]},
      doc:
        "Additional arguments passed to Playwright [Browser.newPage](https://playwright.dev/docs/api/class-browser#browser-new-page).\n" <>
          ~s(E.g. `[accept_downloads: false]`)
    ],
    selector_engines: [
      default: [],
      type: {:or, [:map, :keyword_list]},
      doc:
        "Define custom Playwright [selector engines](https://playwright.dev/docs/extensibility#custom-selector-engines)."
    ]
  )

setup_all_keys = ~w(browser browser_launch_timeout executable_path headless slow_mo)a
setup_keys = ~w(accept_dialogs screenshot trace browser_context_opts browser_page_opts)a

defmodule PhoenixTest.Playwright.Config do
  @moduledoc """
  Configuration options for the Playwright driver.

  Most should be set globally in `config/tests.exs`.
  Some can be overridden per test.

  All options:
  #{NimbleOptions.docs(schema)}

  Options that be overridden per test module via the `use PhoenixTest.Playwright.Case` opts:
  #{Enum.map_join(setup_all_keys, "\n", &"- `:#{&1}`")}

  Options that be overridden per test via ExUnit `@tag`:
  #{Enum.map_join(setup_keys, "\n", &"- `:#{&1}`")}
  """

  @schema schema
  @screenshot_opts_schema screenshot_opts_schema
  @setup_all_keys setup_all_keys
  @setup_keys setup_keys
  @playwright_recommended_version playwright_recommended_version

  def validate!(config) when is_map(config), do: config |> Keyword.new() |> validate!()

  def validate!(config) when is_list(config) do
    global()
    |> Keyword.merge(config)
    |> NimbleOptions.validate!(@schema)
    |> normalize()
  end

  def global do
    :phoenix_test
    |> Application.get_env(:playwright, [])
    |> NimbleOptions.validate!(@schema)
    |> normalize()
  end

  def global(key), do: Keyword.fetch!(global(), key)

  def setup_all_keys, do: @setup_all_keys
  def setup_keys, do: @setup_keys

  defp normalize(config), do: Keyword.new(config, fn {key, value} -> {key, normalize(key, value)} end)

  defp normalize(:screenshot, true), do: NimbleOptions.validate!([], @screenshot_opts_schema)
  defp normalize(_key, value), do: value

  def __validate_runner__(runner) do
    if executable = System.find_executable(runner) do
      {:ok, executable}
    else
      message = """
      could not find runner executable at `#{runner}`.

      To resolve this please
      1. Install a JS package runner like `npx` or `bunx`
      2. Configure the preferred runner in `config/test.exs`, e.g.: `config :phoenix_test, playwright: [runner: "npx"]`
      3. Ensure either the runner is in your PATH or the `runner` value is a absolute path to the executable (e.g. `Path.absname("_build/bun")`)
      """

      {:error, message}
    end
  end

  def __validate_assets_dir__(assets_dir) do
    playwright_json = Path.join([assets_dir, "node_modules", "playwright", "package.json"])

    with {:ok, string} <- File.read(playwright_json),
         {:ok, json} <- JSON.decode(string) do
      version = json["version"] || "0"

      if Version.compare(version, @playwright_recommended_version) == :lt do
        IO.warn("Playwright version #{version} is below recommended #{@playwright_recommended_version}")
      end

      {:ok, assets_dir}
    else
      {:error, error} ->
        message = """
        could not find playwright in `#{assets_dir}`.
        Reason: #{inspect(error)}

        To resolve this please
        1. Install playwright, e.g. via `npm --prefix assets install playwright`
        """

        {:error, message}
    end
  end

  def __validate_cli__(_cli) do
    {:error,
     "it is deprecated. Use `assets_dir` instead if you want to customize the Playwright installation directory path and remove the `cli` option."}
  end
end
