defmodule AprsmeWeb.CoreComponentsTest do
  use ExUnit.Case, async: true

  import Phoenix.LiveViewTest

  alias AprsmeWeb.CoreComponents
  alias Phoenix.LiveView.JS

  describe "icon/1" do
    test "renders an inline SVG for a valid heroicon" do
      html = render_component(&CoreComponents.icon/1, name: "arrow-left")
      # Either an SVG or the "icon not found" comment is returned.
      # credo:disable-for-next-line Jump.CredoChecks.ConditionalAssertion
      assert html =~ "<svg" or html =~ "icon not found"
      # icon returns SVG or fallback comment depending on validity
    end

    test "renders a 'not found' comment for unknown icons" do
      html = render_component(&CoreComponents.icon/1, name: "nope-not-a-real-icon")
      assert html =~ "icon not found: nope-not-a-real-icon"
    end

    test "falls back to outline=true by default" do
      # With a valid heroicon, the outline variant is selected.
      html = render_component(&CoreComponents.icon/1, name: "x-mark")
      assert String.length(html) > 0
    end

    test "honours custom class" do
      html = render_component(&CoreComponents.icon/1, name: "x-mark", class: "my-special-class")
      # Either inserted into the SVG tag or absent for missing icons.
      assert String.length(html) > 0
    end
  end

  describe "button/1" do
    test "renders a native <button> with the default classes" do
      html = render_component(&CoreComponents.button/1, %{inner_block: text_slot("Save")})
      assert html =~ "<button"
      assert html =~ "Save"
    end

    test "passes through custom class" do
      html =
        render_component(&CoreComponents.button/1, %{
          inner_block: text_slot("Click"),
          class: "custom-class"
        })

      assert html =~ "custom-class"
    end
  end

  describe "label/1" do
    test "renders a <label> wrapping the inner block" do
      html =
        render_component(&CoreComponents.label/1, %{
          for: "name",
          inner_block: text_slot("Name")
        })

      assert html =~ "<label"
      assert html =~ ~s(for="name")
      assert html =~ "Name"
    end
  end

  describe "error/1" do
    test "renders the error message and the default heroicon marker" do
      html = render_component(&CoreComponents.error/1, %{inner_block: text_slot("is required")})
      assert html =~ "is required"
    end
  end

  describe "header/1" do
    test "renders the site header with navigation links" do
      html = render_component(&CoreComponents.header/1, %{})
      # This is the site header component (not a generic header), so it
      # always emits the nav links regardless of the assigns.
      assert html =~ "Home"
      assert html =~ "About"
      assert html =~ "aprs.me"
    end
  end

  describe "flash/1" do
    test "renders an info flash with title and message" do
      html =
        render_component(&CoreComponents.flash/1, %{
          id: "flash-1",
          kind: :info,
          title: "Saved",
          flash: %{"info" => "Your changes have been saved"},
          close: true,
          autoshow: false
        })

      assert html =~ "Saved"
      assert html =~ "Your changes have been saved"
      assert html =~ ~s(id="flash-1")
      assert html =~ "bg-emerald-50"
    end

    test "renders an error flash with rose styling" do
      html =
        render_component(&CoreComponents.flash/1, %{
          id: "flash-err",
          kind: :error,
          title: nil,
          flash: %{"error" => "Something went wrong"},
          close: true,
          autoshow: false
        })

      assert html =~ "Something went wrong"
      assert html =~ "bg-rose-50"
    end

    test "omits the close button when close: false" do
      html =
        render_component(&CoreComponents.flash/1, %{
          id: "flash-2",
          kind: :info,
          title: nil,
          flash: %{"info" => "hi"},
          close: false,
          autoshow: false
        })

      refute html =~ ~s(aria-label="close")
    end
  end

  describe "table/1 row id branches via safe_row_id" do
    test "uses string-keyed 'id' when present" do
      html =
        render_component(&CoreComponents.table/1, %{
          id: "rows",
          rows: [%{"id" => "row-string", "name" => "X"}],
          col: [%{label: "Name", inner_block: fn _, row -> Map.get(row, "name") end}],
          action: [],
          row_click: nil,
          row_id: nil,
          row_item: & &1,
          rest: %{}
        })

      assert html =~ "row-string"
    end

    test "falls back to MD5 hash when row map has no id key" do
      html =
        render_component(&CoreComponents.table/1, %{
          id: "rows-noid",
          rows: [%{name: "noid"}],
          col: [%{label: "Name", inner_block: fn _, row -> Map.get(row, :name) end}],
          action: [],
          row_click: nil,
          row_id: nil,
          row_item: & &1,
          rest: %{}
        })

      # The rendered HTML must include the rows table without crashing.
      assert html =~ "rows-noid"
    end

    test "delegates non-map rows to Phoenix.Param" do
      # Phoenix.Param.to_param("string-row") returns the string itself.
      html =
        render_component(&CoreComponents.table/1, %{
          id: "string-rows",
          rows: ["alpha", "beta"],
          col: [%{label: "Value", inner_block: fn _, row -> row end}],
          action: [],
          row_click: nil,
          row_id: nil,
          row_item: & &1,
          rest: %{}
        })

      assert html =~ "alpha"
      assert html =~ "beta"
    end
  end

  describe "input/1 with a {form, field} tuple" do
    test "extracts name/id/value/errors from the form via the {form, field} clause" do
      # Builds a Phoenix.HTML.Form via Plug.Conn-less helpers — Form.from_struct
      # gives a minimal form value to thread through input/1's first clause.
      f = Phoenix.Component.to_form(%{"username" => "alice"}, as: :user)

      html =
        render_component(&CoreComponents.input/1, %{
          field: {f, :username},
          type: "text",
          label: "Username",
          multiple: false,
          errors: []
        })

      assert html =~ ~s(name="user[username]")
      assert html =~ "alice"
    end

    test "appends [] to the name when multiple is true" do
      f = Phoenix.Component.to_form(%{"tags" => []}, as: :user)

      html =
        render_component(&CoreComponents.input/1, %{
          field: {f, :tags},
          type: "select",
          label: "Tags",
          multiple: true,
          options: [{"Foo", "foo"}, {"Bar", "bar"}],
          errors: [],
          prompt: nil
        })

      assert html =~ ~s(name="user[tags][])
    end
  end

  describe "input/1 checkbox" do
    test "renders a checkbox with a hidden false companion input" do
      html =
        render_component(&CoreComponents.input/1, %{
          id: "enabled",
          name: "enabled",
          type: "checkbox",
          value: "true",
          errors: []
        })

      assert html =~ ~s(type="hidden")
      assert html =~ ~s(type="checkbox")
      # Checkbox is pre-checked when value matches "true".
      assert html =~ "checked"
    end

    test "checkbox is unchecked when value is not 'true'" do
      html =
        render_component(&CoreComponents.input/1, %{
          id: "flag",
          name: "flag",
          type: "checkbox",
          value: "false",
          errors: []
        })

      refute html =~ " checked "
    end
  end

  describe "input/1 select" do
    test "renders a select with options" do
      html =
        render_component(&CoreComponents.input/1, %{
          id: "color",
          name: "color",
          type: "select",
          label: "Color",
          value: "red",
          errors: [],
          options: [{"Red", "red"}, {"Blue", "blue"}],
          multiple: false,
          prompt: nil
        })

      assert html =~ "<select"
      assert html =~ "Red"
      assert html =~ "Blue"
    end

    test "renders the prompt when given" do
      html =
        render_component(&CoreComponents.input/1, %{
          id: "color",
          name: "color",
          type: "select",
          label: "Color",
          value: nil,
          errors: [],
          options: [{"Red", "red"}],
          multiple: false,
          prompt: "Pick one"
        })

      assert html =~ "Pick one"
    end
  end

  describe "input/1 textarea" do
    test "renders a textarea with the value as content" do
      html =
        render_component(&CoreComponents.input/1, %{
          id: "bio",
          name: "bio",
          type: "textarea",
          label: "Bio",
          value: "hello world",
          errors: []
        })

      assert html =~ "<textarea"
      assert html =~ "hello world"
    end

    test "applies error styling when errors are present" do
      html =
        render_component(&CoreComponents.input/1, %{
          id: "bio",
          name: "bio",
          type: "textarea",
          label: "Bio",
          value: "",
          errors: ["is required"]
        })

      assert html =~ "border-rose-400"
      assert html =~ "is required"
    end
  end

  describe "input/1 generic" do
    test "renders a text input with errors" do
      html =
        render_component(&CoreComponents.input/1, %{
          id: "email",
          name: "email",
          type: "text",
          label: "Email",
          value: "",
          errors: ["is invalid"]
        })

      assert html =~ ~s(name="email")
      assert html =~ "is invalid"
      assert html =~ "border-rose-400"
    end

    test "uses no-error styling when errors are empty" do
      html =
        render_component(&CoreComponents.input/1, %{
          id: "email",
          name: "email",
          type: "text",
          label: "Email",
          value: "a@b",
          errors: []
        })

      assert html =~ "border-zinc-300"
      refute html =~ "border-rose-400"
    end
  end

  describe "show/1 and hide/1" do
    test "show/1 composes onto a JS struct" do
      result = CoreComponents.show(%JS{}, "#foo")
      assert %JS{} = result
    end

    test "hide/1 composes onto a JS struct" do
      result = CoreComponents.hide(%JS{}, "#foo")
      assert %JS{} = result
    end
  end

  describe "show_modal/2 and hide_modal/2" do
    test "show_modal returns a JS struct" do
      assert %JS{} = CoreComponents.show_modal("my-modal")
    end

    test "hide_modal returns a JS struct" do
      assert %JS{} = CoreComponents.hide_modal("my-modal")
    end
  end

  describe "translate_error/1" do
    test "passes plain error tuples through gettext" do
      # No :count opt → uses dgettext.
      result = CoreComponents.translate_error({"is invalid", []})
      assert result =~ "invalid"
    end

    test "uses dngettext for plural errors" do
      result = CoreComponents.translate_error({"should be %{count} character(s)", [count: 5]})
      assert result =~ "5"
    end
  end

  describe "translate_errors/2" do
    test "returns translated errors for the given field" do
      errors = [
        {:name, {"can't be blank", []}},
        {:age, {"is not a number", []}},
        {:name, {"is too short", []}}
      ]

      name_errors = CoreComponents.translate_errors(errors, :name)
      assert length(name_errors) == 2
      assert Enum.all?(name_errors, &is_binary/1)
    end

    test "returns an empty list when no errors match the field" do
      assert CoreComponents.translate_errors([{:other, {"x", []}}], :missing) == []
    end
  end

  describe "modal/1" do
    test "renders a modal with the inner block and close button" do
      html =
        render_component(&CoreComponents.modal/1, %{
          id: "test-modal",
          show: false,
          inner_block: text_slot("Modal body content"),
          title: [],
          subtitle: [],
          confirm: [],
          cancel: [],
          on_confirm: %JS{},
          on_cancel: %JS{}
        })

      assert html =~ ~s(id="test-modal")
      assert html =~ "Modal body content"
      assert html =~ ~s(aria-modal="true")
      assert html =~ ~s(aria-label="close")
    end

    test "renders the title and subtitle slots when supplied" do
      html =
        render_component(&CoreComponents.modal/1, %{
          id: "sample-modal",
          show: false,
          inner_block: text_slot(""),
          title: slot_with_text(:title, "My Modal Title"),
          subtitle: slot_with_text(:subtitle, "My subtitle"),
          confirm: [],
          cancel: [],
          on_confirm: %JS{},
          on_cancel: %JS{}
        })

      assert html =~ "My Modal Title"
      assert html =~ "My subtitle"
    end
  end

  describe "table/1" do
    test "renders a row for each entry in :rows" do
      html =
        render_component(&CoreComponents.table/1, %{
          id: "users",
          rows: [%{id: 1, name: "Alice"}, %{id: 2, name: "Bob"}],
          row_click: nil,
          col: [
            %{
              __slot__: :col,
              label: "Name",
              inner_block: fn _changed, user -> user.name end
            }
          ],
          action: []
        })

      assert html =~ "Alice"
      assert html =~ "Bob"
      assert html =~ ~s(id="users")
      assert html =~ "Name"
    end

    test "renders action slot when provided" do
      html =
        render_component(&CoreComponents.table/1, %{
          id: "users",
          rows: [%{id: 1, name: "Alice"}],
          row_click: nil,
          col: [
            %{
              __slot__: :col,
              label: "Name",
              inner_block: fn _changed, user -> user.name end
            }
          ],
          action: [
            %{
              __slot__: :action,
              inner_block: fn _changed, _user -> "Edit" end
            }
          ]
        })

      assert html =~ "Edit"
    end
  end

  describe "list/1" do
    test "renders a description list with each item" do
      html =
        render_component(&CoreComponents.list/1, %{
          item: [
            %{
              __slot__: :item,
              title: "First",
              inner_block: fn _changed, _ -> "alpha" end
            },
            %{
              __slot__: :item,
              title: "Second",
              inner_block: fn _changed, _ -> "beta" end
            }
          ]
        })

      assert html =~ "First"
      assert html =~ "alpha"
      assert html =~ "Second"
      assert html =~ "beta"
    end
  end

  describe "back/1" do
    test "renders a back link with the given navigate target" do
      html =
        render_component(&CoreComponents.back/1, %{
          navigate: "/somewhere",
          inner_block: text_slot("Go back")
        })

      assert html =~ "Go back"
      assert html =~ ~s(href="/somewhere")
    end
  end

  describe "simple_form/1" do
    test "renders a form with action buttons" do
      html =
        render_component(&CoreComponents.simple_form/1, %{
          for: %{},
          as: :user,
          rest: %{},
          inner_block: text_slot("form fields"),
          actions: [
            %{
              __slot__: :actions,
              inner_block: fn _changed, _ -> "Save" end
            }
          ]
        })

      assert html =~ "<form"
      assert html =~ "form fields"
      assert html =~ "Save"
    end
  end

  describe "header/1 with dev mode" do
    test "shows the DEV badge when dev_mode is true" do
      html = render_component(&CoreComponents.header/1, %{dev_mode: true})
      assert html =~ "DEV"
    end

    test "omits the DEV badge by default" do
      html = render_component(&CoreComponents.header/1, %{})
      refute html =~ "\">DEV</"
    end
  end

  describe "theme_selector/1" do
    test "renders the theme dropdown" do
      html = render_component(&CoreComponents.theme_selector/1, %{})
      # credo:disable-for-next-line Jump.CredoChecks.ConditionalAssertion
      assert html =~ "theme-menu" or html =~ "theme"
      # theme_selector renders dropdown or fallback text
    end
  end

  # Helper: build a LiveView inner_block slot that just emits text.
  defp text_slot(text) do
    [%{__slot__: :inner_block, inner_block: fn _changed, _ -> text end}]
  end

  defp slot_with_text(name, text) do
    [%{__slot__: name, inner_block: fn _changed, _ -> text end}]
  end
end
