defmodule Berrypod.PagesTest do
  use Berrypod.DataCase, async: false

  alias Berrypod.Pages
  alias Berrypod.Pages.{Page, BlockTypes, Defaults, PageCache}

  setup do
    # Clear cached pages between tests so save_page side effects don't leak
    PageCache.invalidate_all()
    :ok
  end

  describe "get_page/1" do
    test "returns defaults when nothing in DB" do
      page = Pages.get_page("home")

      assert page.slug == "home"
      assert page.title == "Home page"
      assert is_list(page.blocks)
      assert length(page.blocks) == 4

      types = Enum.map(page.blocks, & &1["type"])
      assert types == ["hero", "category_nav", "featured_products", "image_text"]
    end

    test "returns DB version when page has been saved" do
      {:ok, _} =
        Pages.save_page("home", %{
          title: "Custom home",
          blocks: [%{"id" => "blk_test", "type" => "hero", "settings" => %{}}]
        })

      page = Pages.get_page("home")

      assert page.slug == "home"
      assert page.title == "Custom home"
      assert length(page.blocks) == 1
    end

    test "each block has an id, type, and settings" do
      page = Pages.get_page("contact")

      for block <- page.blocks do
        assert is_binary(block["id"]), "block missing id"
        assert is_binary(block["type"]), "block missing type"
        assert is_map(block["settings"]), "block missing settings"
      end
    end

    test "returns nil for unknown custom slug" do
      assert Pages.get_page("does-not-exist") == nil
    end

    test "returns page data for created custom page" do
      {:ok, _} = Pages.create_custom_page(%{slug: "my-page", title: "My page"})
      page = Pages.get_page("my-page")

      assert page.slug == "my-page"
      assert page.title == "My page"
      assert page.type == "custom"
    end
  end

  describe "list_system_pages/0" do
    test "returns all 14 pages" do
      pages = Pages.list_system_pages()

      assert length(pages) == 14
      slugs = Enum.map(pages, & &1.slug)
      assert "home" in slugs
      assert "pdp" in slugs
      assert "error" in slugs
    end
  end

  describe "list_custom_pages/0" do
    test "returns empty list when no custom pages exist" do
      assert Pages.list_custom_pages() == []
    end

    test "returns custom pages ordered by title" do
      {:ok, _} = Pages.create_custom_page(%{slug: "zebra", title: "Zebra"})
      {:ok, _} = Pages.create_custom_page(%{slug: "apple", title: "Apple"})
      {:ok, _} = Pages.create_custom_page(%{slug: "mango", title: "Mango"})

      pages = Pages.list_custom_pages()
      titles = Enum.map(pages, & &1.title)
      assert titles == ["Apple", "Mango", "Zebra"]
    end

    test "does not include system pages" do
      {:ok, _} = Pages.save_page("home", %{title: "Home page", blocks: []})
      {:ok, _} = Pages.create_custom_page(%{slug: "custom-one", title: "Custom"})

      pages = Pages.list_custom_pages()
      slugs = Enum.map(pages, & &1.slug)
      assert "custom-one" in slugs
      refute "home" in slugs
    end
  end

  describe "list_all_pages/0" do
    test "includes both system and custom pages" do
      {:ok, _} = Pages.create_custom_page(%{slug: "faq", title: "FAQ"})

      pages = Pages.list_all_pages()
      slugs = Enum.map(pages, & &1.slug)
      assert "home" in slugs
      assert "faq" in slugs
    end
  end

  describe "save_page/2" do
    test "creates a new page in the DB" do
      blocks = [%{"id" => "blk_1", "type" => "hero", "settings" => %{"title" => "Hello"}}]

      assert {:ok, page} = Pages.save_page("about", %{title: "About us", blocks: blocks})

      assert page.slug == "about"
      assert page.title == "About us"
      assert length(page.blocks) == 1
    end

    test "updates an existing page" do
      {:ok, _} = Pages.save_page("about", %{title: "About v1", blocks: []})
      {:ok, page} = Pages.save_page("about", %{title: "About v2", blocks: []})

      assert page.title == "About v2"
      assert Repo.aggregate(from(p in Page, where: p.slug == "about"), :count) == 1
    end

    test "invalidates cache on save" do
      # Prime the cache
      _page = Pages.get_page("home")

      # Save a new version
      {:ok, _} = Pages.save_page("home", %{title: "New home", blocks: []})

      # Should return the new version
      page = Pages.get_page("home")
      assert page.title == "New home"
    end

    test "rejects invalid slug format" do
      assert {:error, changeset} =
               Pages.save_page("UPPER CASE", %{title: "Bad", blocks: []})

      assert errors_on(changeset).slug
    end
  end

  describe "create_custom_page/1" do
    test "creates a custom page with valid slug" do
      assert {:ok, page} = Pages.create_custom_page(%{slug: "our-story", title: "Our story"})

      assert page.slug == "our-story"
      assert page.title == "Our story"
      assert page.type == "custom"
      assert page.blocks == []
      assert page.published == true
    end

    test "rejects duplicate slug" do
      {:ok, _} = Pages.create_custom_page(%{slug: "faq", title: "FAQ"})
      assert {:error, changeset} = Pages.create_custom_page(%{slug: "faq", title: "FAQ 2"})
      assert errors_on(changeset).slug
    end

    test "rejects system slug" do
      assert {:error, changeset} = Pages.create_custom_page(%{slug: "home", title: "Home"})
      assert errors_on(changeset).slug
    end

    test "rejects reserved path" do
      assert {:error, changeset} = Pages.create_custom_page(%{slug: "admin", title: "Admin"})
      assert errors_on(changeset).slug
    end

    test "rejects invalid slug format" do
      assert {:error, changeset} =
               Pages.create_custom_page(%{slug: "My Page", title: "My page"})

      assert errors_on(changeset).slug
    end

    test "rejects slug with leading hyphen" do
      assert {:error, changeset} = Pages.create_custom_page(%{slug: "-bad", title: "Bad"})
      assert errors_on(changeset).slug
    end

    test "rejects slug with trailing hyphen" do
      assert {:error, changeset} = Pages.create_custom_page(%{slug: "bad-", title: "Bad"})
      assert errors_on(changeset).slug
    end

    test "accepts single-character slug" do
      assert {:ok, page} = Pages.create_custom_page(%{slug: "x", title: "X"})
      assert page.slug == "x"
    end

    test "populates cache on create" do
      {:ok, _} = Pages.create_custom_page(%{slug: "cached-page", title: "Cached"})
      assert {:ok, data} = PageCache.get("cached-page")
      assert data.title == "Cached"
      assert data.type == "custom"
    end
  end

  describe "delete_custom_page/1" do
    test "deletes a custom page" do
      {:ok, _} = Pages.create_custom_page(%{slug: "temp", title: "Temp"})
      page_struct = Pages.get_page_struct("temp")
      assert {:ok, _} = Pages.delete_custom_page(page_struct)
      assert Pages.get_page("temp") == nil
    end

    test "refuses to delete a system page" do
      {:ok, page} = Pages.save_page("home", %{title: "Home page", blocks: []})
      assert {:error, :system_page} = Pages.delete_custom_page(page)
    end

    test "invalidates cache on delete" do
      {:ok, _} = Pages.create_custom_page(%{slug: "bye", title: "Bye"})
      page_struct = Pages.get_page_struct("bye")
      Pages.delete_custom_page(page_struct)
      assert :miss = PageCache.get("bye")
    end
  end

  describe "update_custom_page/2" do
    test "updates title" do
      {:ok, _} = Pages.create_custom_page(%{slug: "updates", title: "V1"})
      page = Pages.get_page_struct("updates")
      assert {:ok, updated} = Pages.update_custom_page(page, %{title: "V2"})
      assert updated.title == "V2"
    end

    test "slug change creates redirect" do
      {:ok, _} = Pages.create_custom_page(%{slug: "old-name", title: "Page"})
      page = Pages.get_page_struct("old-name")
      assert {:ok, updated} = Pages.update_custom_page(page, %{slug: "new-name"})
      assert updated.slug == "new-name"

      assert {:ok, redirect} = Berrypod.Redirects.lookup("/old-name")
      assert redirect.to_path == "/new-name"
    end

    test "slug change invalidates old cache and populates new" do
      {:ok, _} = Pages.create_custom_page(%{slug: "rename-me", title: "Page"})
      page = Pages.get_page_struct("rename-me")
      {:ok, _} = Pages.update_custom_page(page, %{slug: "renamed"})

      assert :miss = PageCache.get("rename-me")
      assert {:ok, cached} = PageCache.get("renamed")
      assert cached.slug == "renamed"
    end

    test "updates meta and nav fields" do
      {:ok, _} = Pages.create_custom_page(%{slug: "meta-test", title: "Meta"})
      page = Pages.get_page_struct("meta-test")

      {:ok, updated} =
        Pages.update_custom_page(page, %{
          meta_description: "A test page",
          show_in_nav: true,
          nav_label: "Test",
          nav_position: 5
        })

      assert updated.meta_description == "A test page"
      assert updated.show_in_nav == true
      assert updated.nav_label == "Test"
      assert updated.nav_position == 5
    end

    test "refuses to update a system page" do
      {:ok, page} = Pages.save_page("home", %{title: "Home page", blocks: []})
      assert {:error, :system_page} = Pages.update_custom_page(page, %{title: "Nope"})
    end
  end

  describe "reset_page/1" do
    test "restores defaults after customisation" do
      {:ok, _} =
        Pages.save_page("home", %{
          title: "Custom",
          blocks: [%{"id" => "blk_1", "type" => "hero", "settings" => %{}}]
        })

      assert :ok = Pages.reset_page("home")

      page = Pages.get_page("home")
      assert page.title == "Home page"
      assert length(page.blocks) == 4
    end

    test "is a no-op for a page that was never saved" do
      assert :ok = Pages.reset_page("cart")
    end
  end

  describe "load_block_data/2" do
    test "returns empty map when no blocks have data loaders" do
      blocks = [
        %{"type" => "hero", "settings" => %{}},
        %{"type" => "category_nav", "settings" => %{}}
      ]

      assert Pages.load_block_data(blocks, %{}) == %{}
    end

    test "loads review data when reviews_section block is present" do
      blocks = [%{"type" => "reviews_section", "settings" => %{}}]
      data = Pages.load_block_data(blocks, %{mode: :preview})

      assert is_list(data.reviews)
      assert Decimal.equal?(data.average_rating, Decimal.new(5))
      assert data.review_count == 2
    end

    test "loads featured products with configurable count" do
      blocks = [%{"type" => "featured_products", "settings" => %{"product_count" => 4}}]
      data = Pages.load_block_data(blocks, %{mode: :preview})

      assert is_list(data.products)
    end

    test "loads related products in preview mode" do
      blocks = [%{"type" => "related_products", "settings" => %{}}]
      data = Pages.load_block_data(blocks, %{mode: :preview})

      assert is_list(data.related_products)
    end

    test "loads related products from DB with product context" do
      blocks = [%{"type" => "related_products", "settings" => %{}}]
      data = Pages.load_block_data(blocks, %{product: %{category: "Prints", id: "fake-id"}})

      assert is_list(data.related_products)
    end

    test "merges data from multiple loaders" do
      blocks = [
        %{"type" => "featured_products", "settings" => %{}},
        %{"type" => "reviews_section", "settings" => %{}}
      ]

      data = Pages.load_block_data(blocks, %{mode: :preview})

      assert Map.has_key?(data, :products)
      assert Map.has_key?(data, :reviews)
    end
  end

  describe "BlockTypes" do
    test "all/0 returns all block types" do
      types = BlockTypes.all()

      assert is_map(types)
      assert map_size(types) > 20
      assert Map.has_key?(types, "hero")
      assert Map.has_key?(types, "product_hero")
      assert Map.has_key?(types, "search_results")
    end

    test "get/1 returns a block type definition" do
      hero = BlockTypes.get("hero")

      assert hero.name == "Hero banner"
      assert hero.icon == "hero-megaphone"
      assert hero.allowed_on == :all
      assert is_list(hero.settings_schema)
    end

    test "get/1 returns nil for unknown type" do
      assert BlockTypes.get("nonexistent") == nil
    end

    test "allowed_for/1 returns portable + page-specific blocks" do
      pdp_types = BlockTypes.allowed_for("pdp")

      # Portable blocks are included
      assert Map.has_key?(pdp_types, "hero")
      assert Map.has_key?(pdp_types, "featured_products")

      # PDP-specific blocks are included
      assert Map.has_key?(pdp_types, "product_hero")
      assert Map.has_key?(pdp_types, "breadcrumb")

      # Other page-specific blocks are excluded
      refute Map.has_key?(pdp_types, "cart_items")
      refute Map.has_key?(pdp_types, "contact_form")
    end

    test "allowed_for/1 includes only relevant page-specific blocks" do
      cart_types = BlockTypes.allowed_for("cart")

      assert Map.has_key?(cart_types, "cart_items")
      assert Map.has_key?(cart_types, "order_summary")
      refute Map.has_key?(cart_types, "product_hero")
    end

    test "allowed_for/1 returns only portable blocks for custom pages" do
      custom_types = BlockTypes.allowed_for("my-custom-page")

      # Portable blocks are included
      assert Map.has_key?(custom_types, "hero")
      assert Map.has_key?(custom_types, "featured_products")
      assert Map.has_key?(custom_types, "image_text")

      # Page-specific blocks are excluded
      refute Map.has_key?(custom_types, "cart_items")
      refute Map.has_key?(custom_types, "product_hero")
      refute Map.has_key?(custom_types, "contact_form")
    end

    test "every block type has required fields" do
      for {type, def} <- BlockTypes.all() do
        assert is_binary(def.name), "#{type} missing name"
        assert is_binary(def.icon), "#{type} missing icon"
        assert def.allowed_on == :all or is_list(def.allowed_on), "#{type} bad allowed_on"
        assert is_list(def.settings_schema), "#{type} missing settings_schema"
      end
    end

    test "settings_schema entries have required fields" do
      for {type, def} <- BlockTypes.all(), setting <- def.settings_schema do
        assert is_binary(setting.key), "#{type} setting missing key"
        assert is_binary(setting.label), "#{type} setting missing label"
        assert is_atom(setting.type), "#{type} setting missing type"
        assert Map.has_key?(setting, :default), "#{type} setting #{setting.key} missing default"
      end
    end
  end

  describe "Defaults" do
    test "all/0 returns 14 page definitions" do
      all = Defaults.all()
      assert length(all) == 14
    end

    test "every default page has blocks matching registered types" do
      for page <- Defaults.all() do
        assert is_binary(page.slug), "page missing slug"
        assert is_binary(page.title), "page missing title"
        assert is_list(page.blocks), "#{page.slug} blocks is not a list"

        for block <- page.blocks do
          assert BlockTypes.get(block["type"]),
                 "#{page.slug} has unknown block type: #{block["type"]}"
        end
      end
    end

    test "every default block type is allowed on its page" do
      for page <- Defaults.all() do
        allowed = BlockTypes.allowed_for(page.slug)

        for block <- page.blocks do
          assert Map.has_key?(allowed, block["type"]),
                 "#{block["type"]} not allowed on #{page.slug}"
        end
      end
    end

    test "block IDs are unique within each page" do
      for page <- Defaults.all() do
        ids = Enum.map(page.blocks, & &1["id"])
        assert ids == Enum.uniq(ids), "duplicate block IDs on #{page.slug}"
      end
    end

    test "home page has the expected blocks" do
      page = Defaults.for_slug("home")

      types = Enum.map(page.blocks, & &1["type"])
      assert types == ["hero", "category_nav", "featured_products", "image_text"]
    end

    test "pdp page has the expected blocks" do
      page = Defaults.for_slug("pdp")

      types = Enum.map(page.blocks, & &1["type"])

      assert types == [
               "breadcrumb",
               "product_hero",
               "trust_badges",
               "product_details",
               "reviews_section",
               "related_products"
             ]
    end

    test "contact page has the expected blocks" do
      page = Defaults.for_slug("contact")

      types = Enum.map(page.blocks, & &1["type"])

      assert types == [
               "hero",
               "contact_form",
               "order_tracking_card",
               "info_card",
               "newsletter_card",
               "social_links_card"
             ]
    end

    test "unknown slug returns humanised title and empty blocks" do
      page = Defaults.for_slug("our-story")

      assert page.slug == "our-story"
      assert page.title == "Our story"
      assert page.blocks == []
    end

    test "generate_block_id/0 produces unique prefixed IDs" do
      id1 = Defaults.generate_block_id()
      id2 = Defaults.generate_block_id()

      assert String.starts_with?(id1, "blk_")
      assert String.starts_with?(id2, "blk_")
      assert id1 != id2
    end
  end

  describe "PageCache" do
    test "get/1 returns :miss for uncached slug" do
      assert :miss = PageCache.get("nonexistent_slug_#{System.unique_integer()}")
    end

    test "put/2 and get/1 round-trip" do
      slug = "test_cache_#{System.unique_integer()}"
      page_data = %{slug: slug, title: "Test", blocks: []}

      assert :ok = PageCache.put(slug, page_data)
      assert {:ok, ^page_data} = PageCache.get(slug)
    end

    test "invalidate/1 removes cached entry" do
      slug = "test_invalidate_#{System.unique_integer()}"
      PageCache.put(slug, %{slug: slug, title: "Test", blocks: []})

      assert :ok = PageCache.invalidate(slug)
      assert :miss = PageCache.get(slug)
    end
  end

  describe "Page schema" do
    test "system_slugs/0 returns 14 slugs" do
      assert length(Page.system_slugs()) == 14
    end

    test "system_slug?/1 returns true for system slugs" do
      assert Page.system_slug?("home")
      assert Page.system_slug?("cart")
      refute Page.system_slug?("my-page")
    end

    test "reserved_path?/1 returns true for reserved paths" do
      assert Page.reserved_path?("admin")
      assert Page.reserved_path?("collections")
      refute Page.reserved_path?("faq")
    end
  end

  describe "templates" do
    test "templates/0 returns available templates" do
      templates = Defaults.templates()

      assert length(templates) == 3
      keys = Enum.map(templates, & &1.key)
      assert "blank" in keys
      assert "content" in keys
      assert "landing" in keys
    end

    test "template_blocks/1 returns blocks for content template" do
      blocks = Defaults.template_blocks("content")

      types = Enum.map(blocks, & &1["type"])
      assert types == ["hero", "content_body"]
    end

    test "template_blocks/1 returns blocks for landing template" do
      blocks = Defaults.template_blocks("landing")

      types = Enum.map(blocks, & &1["type"])
      assert "hero" in types
      assert "featured_products" in types
      assert "button" in types
    end

    test "template_blocks/1 returns empty list for blank" do
      assert Defaults.template_blocks("blank") == []
    end

    test "template blocks have unique IDs" do
      for tmpl <- Defaults.templates() do
        blocks = Defaults.template_blocks(tmpl.key)
        ids = Enum.map(blocks, & &1["id"])
        assert ids == Enum.uniq(ids), "duplicate IDs in #{tmpl.key} template"
      end
    end
  end

  describe "update_page_url_slug/2" do
    test "updates url_slug for system page" do
      {:ok, updated} = Pages.update_page_url_slug("about", "our-story")

      assert updated.url_slug == "our-story"
      assert updated.slug == "about"
    end

    test "creates redirect when url_slug changes" do
      {:ok, _} = Pages.update_page_url_slug("about", "our-story")

      assert {:ok, redirect} = Berrypod.Redirects.lookup("/about")
      assert redirect.to_path == "/our-story"
    end

    test "clears url_slug when set to empty string" do
      {:ok, _} = Pages.update_page_url_slug("about", "our-story")
      {:ok, cleared} = Pages.update_page_url_slug("about", "")

      assert cleared.url_slug == nil
    end

    test "creates system page in DB if it doesn't exist yet" do
      # About page hasn't been saved to DB, only exists as defaults
      assert Pages.get_page_struct("delivery") == nil

      {:ok, updated} = Pages.update_page_url_slug("delivery", "shipping")

      assert updated.url_slug == "shipping"
      assert Pages.get_page_struct("delivery") != nil
    end

    test "returns error for non-existent custom page" do
      assert {:error, :not_found} = Pages.update_page_url_slug("nope", "anything")
    end

    test "deletes stale redirect when new URL becomes live" do
      # Create a redirect pointing FROM /my-url
      Berrypod.Redirects.create_auto(%{
        from_path: "/my-url",
        to_path: "/somewhere-else",
        source: "manual"
      })

      # Now make /my-url a live page
      {:ok, _} = Pages.update_page_url_slug("about", "my-url")

      # The stale redirect should be gone
      assert :not_found = Berrypod.Redirects.lookup("/my-url")
    end

    test "invalidates R cache so new URL resolves immediately" do
      {:ok, _} = Pages.update_page_url_slug("cart", "basket")

      # R.cart() should now return /basket
      assert BerrypodWeb.R.cart() == "/basket"
    end
  end

  describe "duplicate_custom_page/1" do
    test "creates a draft copy with -copy slug" do
      {:ok, original} =
        Pages.create_custom_page(%{
          "slug" => "test-original",
          "title" => "Test Page",
          "blocks" => [%{"id" => "blk_1", "type" => "hero", "settings" => %{"title" => "Hello"}}]
        })

      {:ok, copy} = Pages.duplicate_custom_page(original)

      assert copy.slug == "test-original-copy"
      assert copy.title == "Test Page (copy)"
      assert copy.published == false
      assert length(copy.blocks) == 1
      assert hd(copy.blocks)["type"] == "hero"
    end

    test "deduplicates slug when copy already exists" do
      {:ok, original} =
        Pages.create_custom_page(%{"slug" => "dup-test", "title" => "Dup Test"})

      {:ok, _first_copy} = Pages.duplicate_custom_page(original)
      {:ok, second_copy} = Pages.duplicate_custom_page(original)

      assert second_copy.slug == "dup-test-copy-2"
    end
  end
end
