defmodule PhoenixTest.Static do
  @moduledoc false

  import Phoenix.ConnTest

  alias PhoenixTest.ActiveForm
  alias PhoenixTest.ConnHandler
  alias PhoenixTest.DataAttributeForm
  alias PhoenixTest.Element.Button
  alias PhoenixTest.Element.Field
  alias PhoenixTest.Element.Form
  alias PhoenixTest.Element.Link
  alias PhoenixTest.Element.Select
  alias PhoenixTest.FileUpload
  alias PhoenixTest.FormData
  alias PhoenixTest.FormPayload
  alias PhoenixTest.Html
  alias PhoenixTest.Locators
  alias PhoenixTest.OpenBrowser
  alias PhoenixTest.Operation
  alias PhoenixTest.Query

  @endpoint Application.compile_env(:phoenix_test, :endpoint)

  defstruct conn: nil, active_form: ActiveForm.new(), within: :none, current_path: "", current_operation: nil

  def build(conn) do
    %__MODULE__{conn: conn, current_path: ConnHandler.build_current_path(conn)}
  end

  def current_path(session), do: session.current_path

  def render_page_title(session) do
    session
    |> render_html()
    |> Query.find("title")
    |> case do
      {:found, element} -> Html.inner_text(element)
      _ -> nil
    end
  end

  def render_html(%{conn: conn, within: within}) do
    html =
      conn
      |> html_response(conn.status)
      |> Html.parse_document()

    case within do
      :none -> html
      selector when is_binary(selector) -> Html.all(html, selector)
    end
  end

  def click_link(session, selector \\ "a", text) do
    session = set_operation(session, :click_link)
    link = Link.find!(session.current_operation.html, selector, text)

    if Link.has_data_method?(link) do
      form =
        link.parsed
        |> DataAttributeForm.build()
        |> DataAttributeForm.validate!(selector, text)

      perform_submit(session, form, form.data)
    else
      conn = session.conn

      conn
      |> ConnHandler.recycle_all_headers()
      |> PhoenixTest.visit(link.href)
    end
  end

  def click_button(session, text) do
    locator = Locators.button(text: text)
    session = set_operation(session, :click_button)

    button =
      session.current_operation.html
      |> Query.find_by_role!(locator)
      |> Button.build()

    handle_click_button(session, button)
  end

  def click_button(session, selector, text) do
    session = set_operation(session, :click_button)
    button = Button.find!(session.current_operation.html, selector, text)

    handle_click_button(session, button)
  end

  defp handle_click_button(session, button) do
    active_form = session.active_form
    html = session.current_operation.html

    if Button.has_data_method?(button) do
      form =
        button.parsed
        |> DataAttributeForm.build()
        |> DataAttributeForm.validate!(button.selector, button.text)

      perform_submit(session, form, form.data)
    else
      form =
        button
        |> Button.parent_form!(html)
        |> Form.put_button_data(button)

      if active_form.selector == form.selector do
        submit_active_form(session, form)
      else
        perform_submit(session, form, build_payload(form))
      end
    end
  end

  def fill_in(session, label, opts) do
    selectors = ["input:not([type='hidden'])", "textarea"]
    fill_in(session, selectors, label, opts)
  end

  def fill_in(session, input_selector, label, opts) do
    {value, opts} = Keyword.pop!(opts, :with)
    session = set_operation(session, :fill_in)

    session.current_operation.html
    |> Field.find_input!(input_selector, label, opts)
    |> Map.put(:value, to_string(value))
    |> then(&fill_in_field_data(session, &1))
  end

  def select(session, option, opts) do
    select(session, "select", option, opts)
  end

  def select(session, input_selector, option, opts) do
    {label, opts} = Keyword.pop!(opts, :from)
    session = set_operation(session, :select)

    session.current_operation.html
    |> Select.find_select_option!(input_selector, label, option, opts)
    |> then(&fill_in_field_data(session, &1))
  end

  def check(session, label, opts) do
    check(session, "input[type='checkbox']", label, opts)
  end

  def check(session, input_selector, label, opts) do
    session = set_operation(session, :check)

    session.current_operation.html
    |> Field.find_checkbox!(input_selector, label, opts)
    |> then(&fill_in_field_data(session, &1))
  end

  def uncheck(session, label, opts) do
    uncheck(session, "input[type='checkbox']", label, opts)
  end

  def uncheck(session, input_selector, label, opts) do
    session = set_operation(session, :uncheck)

    session.current_operation.html
    |> Field.find_hidden_uncheckbox!(input_selector, label, opts)
    |> then(&fill_in_field_data(session, &1))
  end

  def choose(session, label, opts) do
    choose(session, "input[type='radio']", label, opts)
  end

  def choose(session, input_selector, label, opts) do
    session = set_operation(session, :choose)

    session.current_operation.html
    |> Field.find_input!(input_selector, label, opts)
    |> then(&fill_in_field_data(session, &1))
  end

  def upload(session, label, path, opts) do
    upload(session, "input[type='file']", label, path, opts)
  end

  def upload(session, input_selector, label, path, opts) do
    mime_type = FileUpload.mime_type(path)
    upload = %Plug.Upload{content_type: mime_type, filename: Path.basename(path), path: path}
    session = set_operation(session, :upload)
    html = session.current_operation.html
    field = Field.find_input!(html, input_selector, label, opts)
    form = Field.parent_form!(field, html)
    upload_data = {field.name, upload}

    Map.update!(session, :active_form, fn active_form ->
      if active_form.selector == form.selector do
        ActiveForm.add_upload(active_form, upload_data)
      else
        [id: form.id, selector: form.selector]
        |> ActiveForm.new()
        |> ActiveForm.add_upload(upload_data)
      end
    end)
  end

  def submit(session) do
    active_form = session.active_form

    unless ActiveForm.active?(active_form), do: raise(no_active_form_error())

    selector = active_form.selector
    session = set_operation(session, :submit)

    form =
      session.current_operation.html
      |> Form.find!(selector)
      |> then(fn form ->
        Form.put_button_data(form, form.submit_button)
      end)

    submit_active_form(session, form)
  end

  def submit_form(session, selector, form_data) do
    session = set_operation!(session, :submit_form)

    form =
      session.current_operation.html
      |> Form.find!(selector)
      |> then(fn form ->
        Form.put_button_data(form, form.submit_button)
      end)

    to_submit = FormPayload.new(FormData.merge(form.form_data, form_data))

    session
    |> Map.put(:active_form, ActiveForm.new())
    |> perform_submit(form, to_submit)
  end

  def open_browser(session, open_fun \\ &OpenBrowser.open_with_system_cmd/1) do
    path = Path.join([System.tmp_dir!(), "phx-test#{System.unique_integer([:monotonic])}.html"])

    html =
      session.conn.resp_body
      |> Html.parse_document()
      |> Html.postwalk(&OpenBrowser.prefix_static_paths(&1, @endpoint))
      |> Html.raw()

    File.write!(path, html)

    open_fun.(path)

    session
  end

  def unwrap(%{conn: conn} = session, fun) when is_function(fun, 1) do
    conn
    |> fun.()
    |> maybe_redirect(session)
  end

  defp fill_in_field_data(session, field) do
    Field.validate_name!(field)
    form = Field.parent_form!(field, session.current_operation.html)

    Map.update!(session, :active_form, fn active_form ->
      if active_form.selector == form.selector do
        ActiveForm.add_form_data(active_form, field)
      else
        [id: form.id, selector: form.selector]
        |> ActiveForm.new()
        |> ActiveForm.add_form_data(field)
      end
    end)
  end

  defp submit_active_form(session, form) do
    active_form = session.active_form

    session
    |> Map.put(:active_form, ActiveForm.new())
    |> perform_submit(form, build_payload(form, active_form))
  end

  defp perform_submit(session, form, payload) do
    conn = session.conn

    conn
    |> ConnHandler.recycle_all_headers()
    |> dispatch(@endpoint, form.method, form.action, payload)
    |> maybe_redirect(session)
  end

  defp no_active_form_error do
    %ArgumentError{
      message: "There's no active form. Fill in a form with `fill_in`, `select`, etc."
    }
  end

  defp build_payload(form, active_form \\ ActiveForm.new()) do
    form.form_data
    |> FormData.merge(active_form.form_data)
    |> FormPayload.new()
    |> FormPayload.add_form_data(active_form.uploads)
  end

  defp maybe_redirect(conn, session) do
    case conn do
      %{status: 302} ->
        path = redirected_to(conn)

        conn
        |> ConnHandler.recycle_all_headers()
        |> PhoenixTest.visit(path)

      %{status: _} ->
        %{session | conn: conn, current_path: ConnHandler.build_current_path(conn)}
    end
  end

  defp set_operation(session, name, rendered_html \\ nil) do
    html = rendered_html || render_html(session)
    Map.put(session, :current_operation, Operation.new(name, html))
  end

  defp set_operation!(session, name) do
    case session.current_operation do
      %Operation{} -> session
      nil -> set_operation(session, name)
    end
  end
end

defimpl PhoenixTest.Driver, for: PhoenixTest.Static do
  alias PhoenixTest.Assertions
  alias PhoenixTest.ConnHandler
  alias PhoenixTest.SessionHelpers
  alias PhoenixTest.Static

  def visit(session, path) do
    ConnHandler.visit(session.conn, path)
  end

  defdelegate render_page_title(session), to: Static
  defdelegate render_html(session), to: Static
  defdelegate click_link(session, text), to: Static
  defdelegate click_link(session, selector, text), to: Static
  defdelegate click_button(session, text), to: Static
  defdelegate click_button(session, selector, text), to: Static
  defdelegate within(session, selector, fun), to: SessionHelpers
  defdelegate fill_in(session, label, opts), to: Static
  defdelegate fill_in(session, input_selector, label, opts), to: Static
  defdelegate select(session, option, opts), to: Static
  defdelegate select(session, input_selector, option, opts), to: Static
  defdelegate check(session, label, opts), to: Static
  defdelegate check(session, input_selector, label, opts), to: Static
  defdelegate uncheck(session, label, opts), to: Static
  defdelegate uncheck(session, input_selector, label, opts), to: Static
  defdelegate choose(session, label, opts), to: Static
  defdelegate choose(session, input_selector, label, opts), to: Static
  defdelegate upload(session, label, path, opts), to: Static
  defdelegate upload(session, input_selector, label, path, opts), to: Static
  defdelegate submit(session), to: Static
  defdelegate open_browser(session), to: Static
  defdelegate open_browser(session, open_fun), to: Static
  defdelegate unwrap(session, fun), to: Static
  defdelegate current_path(session), to: Static

  defdelegate assert_has(session, selector), to: Assertions
  defdelegate assert_has(session, selector, opts), to: Assertions
  defdelegate refute_has(session, selector), to: Assertions
  defdelegate refute_has(session, selector, opts), to: Assertions
  defdelegate assert_path(session, path), to: Assertions
  defdelegate assert_path(session, path, opts), to: Assertions
  defdelegate refute_path(session, path), to: Assertions
  defdelegate refute_path(session, path, opts), to: Assertions
end
