defmodule Berrypod.NewsletterTest do
  use Berrypod.DataCase, async: true
  use Oban.Testing, repo: Berrypod.Repo

  alias Berrypod.Newsletter
  alias Berrypod.Newsletter.{Campaign, Subscriber}

  import Berrypod.NewsletterFixtures

  # ── Subscribe flow ───────────────────────────────────────────────

  describe "subscribe/2" do
    test "creates a pending subscriber" do
      assert {:ok, %Subscriber{status: "pending"}} =
               Newsletter.subscribe("test@example.com")
    end

    test "normalises email to lowercase" do
      {:ok, sub} = Newsletter.subscribe("Test@EXAMPLE.com")
      assert sub.email == "test@example.com"
    end

    test "stores consent text and source" do
      {:ok, sub} =
        Newsletter.subscribe("test@example.com",
          consent_text: "Stay updated",
          source: "website"
        )

      assert sub.consent_text == "Stay updated"
      assert sub.source == "website"
    end

    test "stores hashed confirmation token" do
      {:ok, sub} = Newsletter.subscribe("test@example.com")
      sub = Repo.get!(Subscriber, sub.id)
      assert sub.confirmation_token != nil
      # Token is a hex-encoded SHA256 hash (64 chars)
      assert byte_size(sub.confirmation_token) == 64
    end

    test "enqueues confirmation email worker" do
      Oban.Testing.with_testing_mode(:manual, fn ->
        {:ok, _sub} = Newsletter.subscribe("test@example.com")

        assert_enqueued(worker: Berrypod.Newsletter.ConfirmationEmailWorker)
      end)
    end

    test "returns already_confirmed for confirmed subscribers" do
      sub = confirmed_subscriber_fixture(email: "test@example.com")

      assert {:already_confirmed, ^sub} =
               Newsletter.subscribe("test@example.com")
    end

    test "returns ok for pending subscribers (resends confirmation)" do
      {:ok, sub} = Newsletter.subscribe("test@example.com")
      assert {:ok, ^sub} = Newsletter.subscribe("test@example.com")
    end

    test "resubscribes unsubscribed users" do
      confirmed_subscriber_fixture(email: "test@example.com")
      {:ok, _} = Newsletter.unsubscribe("test@example.com")

      {:ok, resub} = Newsletter.subscribe("test@example.com")
      assert resub.status == "pending"
      assert resub.unsubscribed_at == nil
    end

    test "rejects invalid email" do
      assert {:error, %Ecto.Changeset{}} = Newsletter.subscribe("not-an-email")
    end
  end

  # ── Confirm ──────────────────────────────────────────────────────

  describe "confirm/1" do
    test "confirms a pending subscriber with valid token" do
      Oban.Testing.with_testing_mode(:manual, fn ->
        {:ok, _sub} = Newsletter.subscribe("test@example.com")

        # Grab the raw token from the enqueued Oban job
        [job] = all_enqueued(worker: Berrypod.Newsletter.ConfirmationEmailWorker)
        raw_token = job.args["raw_token"]

        assert {:ok, confirmed} = Newsletter.confirm(raw_token)
        assert confirmed.status == "confirmed"
        assert confirmed.confirmed_at != nil
        assert confirmed.confirmation_token == nil
      end)
    end

    test "returns error for invalid token" do
      assert {:error, :invalid_token} = Newsletter.confirm("bogus-token")
    end

    test "is idempotent for already confirmed subscribers" do
      Oban.Testing.with_testing_mode(:manual, fn ->
        {:ok, _sub} = Newsletter.subscribe("test@example.com")
        [job] = all_enqueued(worker: Berrypod.Newsletter.ConfirmationEmailWorker)
        raw_token = job.args["raw_token"]

        {:ok, _} = Newsletter.confirm(raw_token)
        # Token is cleared after first confirm, so second call with same token fails
        assert {:error, :invalid_token} = Newsletter.confirm(raw_token)
      end)
    end
  end

  # ── Unsubscribe ──────────────────────────────────────────────────

  describe "unsubscribe/1" do
    test "marks subscriber as unsubscribed" do
      confirmed_subscriber_fixture(email: "test@example.com")

      assert {:ok, sub} = Newsletter.unsubscribe("test@example.com")
      assert sub.status == "unsubscribed"
      assert sub.unsubscribed_at != nil
    end

    test "does not add to global suppression list" do
      confirmed_subscriber_fixture(email: "test@example.com")
      Newsletter.unsubscribe("test@example.com")

      assert Berrypod.Orders.check_suppression("test@example.com") == :ok
    end

    test "is idempotent" do
      confirmed_subscriber_fixture(email: "test@example.com")
      {:ok, _} = Newsletter.unsubscribe("test@example.com")
      assert {:ok, _} = Newsletter.unsubscribe("test@example.com")
    end

    test "returns error for unknown email" do
      assert {:error, :not_found} = Newsletter.unsubscribe("unknown@example.com")
    end
  end

  # ── Subscriber queries ───────────────────────────────────────────

  describe "list_subscribers/1" do
    test "returns all subscribers" do
      confirmed_subscriber_fixture()
      confirmed_subscriber_fixture()

      result = Newsletter.list_subscribers()
      assert length(result) == 2
    end

    test "filters by status" do
      confirmed_subscriber_fixture()
      {:ok, _} = Newsletter.subscribe("pending@example.com")

      result = Newsletter.list_subscribers(status: "confirmed")
      assert length(result) == 1
      assert hd(result).status == "confirmed"
    end

    test "searches by email" do
      confirmed_subscriber_fixture(email: "alice@example.com")
      confirmed_subscriber_fixture(email: "bob@example.com")

      result = Newsletter.list_subscribers(search: "alice")
      assert length(result) == 1
      assert hd(result).email == "alice@example.com"
    end
  end

  describe "count_subscribers_by_status/0" do
    test "returns counts grouped by status" do
      confirmed_subscriber_fixture()
      confirmed_subscriber_fixture()
      {:ok, _} = Newsletter.subscribe("pending@example.com")

      counts = Newsletter.count_subscribers_by_status()
      assert counts["confirmed"] == 2
      assert counts["pending"] == 1
    end
  end

  describe "confirmed_subscriber_count/0" do
    test "returns count of confirmed subscribers" do
      confirmed_subscriber_fixture()
      confirmed_subscriber_fixture()
      {:ok, _} = Newsletter.subscribe("pending@example.com")

      assert Newsletter.confirmed_subscriber_count() == 2
    end
  end

  describe "delete_subscriber/1" do
    test "hard-deletes a subscriber" do
      sub = confirmed_subscriber_fixture()
      assert {:ok, _} = Newsletter.delete_subscriber(sub)
      assert Repo.get(Subscriber, sub.id) == nil
    end
  end

  describe "export_subscriber_data/1" do
    test "returns a map of all stored data" do
      sub = confirmed_subscriber_fixture(email: "export@example.com")
      data = Newsletter.export_subscriber_data(sub)

      assert data.email == "export@example.com"
      assert data.status == "confirmed"
      assert data.subscribed_at != nil
    end
  end

  describe "export_all_subscribers_csv/0" do
    test "returns CSV with header and rows" do
      confirmed_subscriber_fixture(email: "a@example.com")
      confirmed_subscriber_fixture(email: "b@example.com")

      csv = Newsletter.export_all_subscribers_csv()
      assert csv =~ "email,status,confirmed_at,subscribed_at"
      assert csv =~ "a@example.com"
      assert csv =~ "b@example.com"
    end
  end

  # ── Campaign CRUD ────────────────────────────────────────────────

  describe "campaign CRUD" do
    test "creates a draft campaign" do
      assert {:ok, %Campaign{status: "draft"}} =
               Newsletter.create_campaign(%{
                 subject: "Test",
                 body: "Hello {{unsubscribe_url}}"
               })
    end

    test "lists campaigns" do
      campaign_fixture(subject: "First")
      campaign_fixture(subject: "Second")

      result = Newsletter.list_campaigns()
      assert length(result) == 2
    end

    test "updates a campaign" do
      campaign = campaign_fixture()
      {:ok, updated} = Newsletter.update_campaign(campaign, %{subject: "Updated"})
      assert updated.subject == "Updated"
    end

    test "deletes a draft campaign" do
      campaign = campaign_fixture()
      assert {:ok, _} = Newsletter.delete_campaign(campaign)
    end

    test "refuses to delete non-draft campaigns" do
      campaign = campaign_fixture()
      {:ok, sent} = Newsletter.update_campaign(campaign, %{status: "sent"})
      assert {:error, :not_draft} = Newsletter.delete_campaign(sent)
    end
  end

  describe "schedule_campaign/2" do
    test "schedules a draft campaign" do
      campaign = campaign_fixture()
      at = DateTime.utc_now() |> DateTime.add(3600) |> DateTime.truncate(:second)

      assert {:ok, scheduled} = Newsletter.schedule_campaign(campaign, at)
      assert scheduled.status == "scheduled"
      assert scheduled.scheduled_at == at
    end

    test "refuses to schedule non-draft campaigns" do
      campaign = campaign_fixture()
      {:ok, sent} = Newsletter.update_campaign(campaign, %{status: "sent"})
      at = DateTime.utc_now() |> DateTime.add(3600) |> DateTime.truncate(:second)

      assert {:error, :not_draft} = Newsletter.schedule_campaign(sent, at)
    end
  end

  describe "cancel_campaign/1" do
    test "cancels a scheduled campaign" do
      campaign = campaign_fixture()
      at = DateTime.utc_now() |> DateTime.add(3600) |> DateTime.truncate(:second)
      {:ok, scheduled} = Newsletter.schedule_campaign(campaign, at)

      assert {:ok, cancelled} = Newsletter.cancel_campaign(scheduled)
      assert cancelled.status == "cancelled"
    end

    test "refuses to cancel non-scheduled campaigns" do
      campaign = campaign_fixture()
      assert {:error, :not_scheduled} = Newsletter.cancel_campaign(campaign)
    end
  end

  describe "preview_campaign/1" do
    test "replaces unsubscribe placeholder" do
      campaign = campaign_fixture(body: "Hello! Unsub: {{unsubscribe_url}}")
      preview = Newsletter.preview_campaign(campaign)
      assert preview =~ "/unsubscribe/sample-token"
      refute preview =~ "{{unsubscribe_url}}"
    end
  end

  # ── Settings ─────────────────────────────────────────────────────

  describe "newsletter_enabled?/0" do
    test "defaults to false" do
      refute Newsletter.newsletter_enabled?()
    end

    test "returns true when enabled" do
      Newsletter.set_newsletter_enabled(true)
      assert Newsletter.newsletter_enabled?()
    end
  end
end
