defmodule Plausible.Ingestion.RequestTest do
  use Plausible.DataCase, async: true

  import Phoenix.ConnTest
  import Plug.Conn

  alias Plausible.Ingestion.Request

  test "request cannot be built from conn without payload" do
    conn = build_conn(:post, "/api/events", %{})
    assert {:error, changeset} = Request.build(conn)

    errors = Keyword.keys(changeset.errors)
    assert :event_name in errors
    assert :domain in errors
    assert :url in errors
  end

  test "request cannot be built from non-json payload" do
    conn = build_conn(:post, "/api/events", "defnotjson")
    assert {:error, changeset} = Request.build(conn)
    assert changeset.errors[:request]
  end

  test "request can be built from URL alone" do
    payload = %{
      name: "pageview",
      url: "http://dummy.site"
    }

    conn = build_conn(:post, "/api/events", payload)
    assert {:ok, request} = Request.build(conn)

    assert request.domains == ["dummy.site"]
    assert request.event_name == "pageview"
    assert request.pathname == "/"
    assert request.remote_ip == "127.0.0.1"
    assert %NaiveDateTime{} = request.timestamp
    assert request.user_agent == nil
    assert request.hostname == "dummy.site"
    assert request.uri.host == "dummy.site"
    assert request.uri.scheme == "http"
    assert request.props == %{}
  end

  @tag :slow
  test "requests include moving timestamp" do
    payload = %{
      name: "pageview",
      url: "http://dummy.site"
    }

    conn = build_conn(:post, "/api/events", payload)

    assert {:ok, request1} = Request.build(conn)
    :timer.sleep(1500)
    assert {:ok, request2} = Request.build(conn)

    ts1 = request1.timestamp
    ts2 = request2.timestamp

    assert NaiveDateTime.compare(ts1, ts2) == :lt
  end

  test "request can be built with domain" do
    payload = %{
      name: "pageview",
      domain: "dummy.site",
      url: "http://dummy.site/index"
    }

    conn = build_conn(:post, "/api/events", payload)
    assert {:ok, request} = Request.build(conn)
    assert request.domains == ["dummy.site"]
    assert request.uri.host == "dummy.site"
  end

  test "request can be built with domain using shorthands" do
    payload = %{
      n: "pageview",
      d: "dummy.site",
      u: "http://dummy.site/index"
    }

    conn = build_conn(:post, "/api/events", payload)
    assert {:ok, request} = Request.build(conn)
    assert request.domains == ["dummy.site"]
    assert request.uri.host == "dummy.site"
  end

  test "request can be built for multiple domains" do
    payload = %{
      n: "pageview",
      d: "dummy.site,crash.site",
      u: "http://dummy.site/index"
    }

    conn = build_conn(:post, "/api/events", payload)
    assert {:ok, request} = Request.build(conn)
    assert request.domains == ["dummy.site", "crash.site"]
    assert request.uri.host == "dummy.site"
  end

  test "request can be built with numeric event name" do
    payload = %{
      n: 404,
      d: "dummy.site",
      u: "http://dummy.site/index"
    }

    conn = build_conn(:post, "/api/events", payload)
    assert {:ok, request} = Request.build(conn)
    assert request.event_name == "404"
  end

  test "hostname is (none) if host-less uri provided" do
    payload = %{
      name: "pageview",
      domain: "dummy.site",
      url: "about:config"
    }

    conn = build_conn(:post, "/api/events", payload)
    assert {:ok, request} = Request.build(conn)
    assert request.hostname == "(none)"
  end

  test "hostname is set" do
    payload = %{
      name: "pageview",
      domain: "dummy.site",
      url: "http://dummy.site/index.html"
    }

    conn = build_conn(:post, "/api/events", payload)
    assert {:ok, request} = Request.build(conn)
    assert request.hostname == "dummy.site"
  end

  test "user agent is set as-is" do
    payload = %{
      name: "pageview",
      domain: "dummy.site",
      url: "http://dummy.site/index.html"
    }

    conn = build_conn(:post, "/api/events", payload) |> put_req_header("user-agent", "Mozilla")
    assert {:ok, request} = Request.build(conn)
    assert request.user_agent == "Mozilla"
  end

  test "request params are set" do
    payload = %{
      name: "pageview",
      domain: "dummy.site",
      url: "http://dummy.site/index.html",
      referrer: "https://example.com",
      hashMode: 1,
      props: %{
        "custom1" => "property1",
        "custom2" => "property2"
      }
    }

    conn = build_conn(:post, "/api/events", payload)

    assert {:ok, request} = Request.build(conn)
    assert request.referrer == "https://example.com"
    assert request.hash_mode == 1
    assert request.props["custom1"] == "property1"
    assert request.props["custom2"] == "property2"
  end

  @tag :ee_only
  test "parses revenue source field from a json string" do
    payload = %{
      name: "pageview",
      domain: "dummy.site",
      url: "http://dummy.site/index.html",
      revenue: "{\"amount\":20.2,\"currency\":\"EUR\"}"
    }

    conn = build_conn(:post, "/api/events", payload)

    assert {:ok, request} = Request.build(conn)
    assert %Money{amount: amount, currency: :EUR} = request.revenue_source
    assert Decimal.new("20.2") == amount
  end

  @tag :ee_only
  test "sets revenue source with integer amount" do
    payload = %{
      name: "pageview",
      domain: "dummy.site",
      url: "http://dummy.site/index.html",
      revenue: %{
        "amount" => 20,
        "currency" => "USD"
      }
    }

    conn = build_conn(:post, "/api/events", payload)

    assert {:ok, request} = Request.build(conn)
    assert %Money{amount: amount, currency: :USD} = request.revenue_source
    assert Decimal.equal?(amount, Decimal.new("20.0"))
  end

  @tag :ee_only
  test "sets revenue source with float amount" do
    payload = %{
      name: "pageview",
      domain: "dummy.site",
      url: "http://dummy.site/index.html",
      revenue: %{
        "amount" => 20.1,
        "currency" => "USD"
      }
    }

    conn = build_conn(:post, "/api/events", payload)

    assert {:ok, request} = Request.build(conn)
    assert %Money{amount: amount, currency: :USD} = request.revenue_source
    assert Decimal.equal?(amount, Decimal.new("20.1"))
  end

  @tag :ee_only
  test "parses string amounts into money structs" do
    payload = %{
      name: "pageview",
      domain: "dummy.site",
      url: "http://dummy.site/index.html",
      revenue: %{
        "amount" => "12.3",
        "currency" => "USD"
      }
    }

    conn = build_conn(:post, "/api/events", payload)

    assert {:ok, request} = Request.build(conn)
    assert %Money{amount: amount, currency: :USD} = request.revenue_source
    assert Decimal.equal?(amount, Decimal.new("12.3"))
  end

  @tag :ee_only
  test "ignores revenue data when currency is invalid" do
    payload = %{
      name: "pageview",
      domain: "dummy.site",
      url: "http://dummy.site/index.html",
      revenue: %{
        "amount" => 1233.2,
        "currency" => "EEEE"
      }
    }

    conn = build_conn(:post, "/api/events", payload)
    assert {:ok, request} = Request.build(conn)
    assert is_nil(request.revenue_source)
  end

  test "pathname is set" do
    payload = %{
      name: "pageview",
      domain: "dummy.site",
      url: "http://dummy.site/pictures/index.html#foo"
    }

    conn = build_conn(:post, "/api/events", payload)

    assert {:ok, request} = Request.build(conn)

    assert request.pathname == "/pictures/index.html"
  end

  test "pathname is set with hashMode" do
    payload = %{
      name: "pageview",
      domain: "dummy.site",
      url: "http://dummy.site/pictures/index.html#foo",
      hashMode: 1
    }

    conn = build_conn(:post, "/api/events", payload)

    assert {:ok, request} = Request.build(conn)

    assert request.pathname == "/pictures/index.html#foo"
  end

  test "query params are set" do
    payload = %{
      name: "pageview",
      domain: "dummy.site",
      url: "http://dummy.site/pictures/index.html?foo=bar&baz=bam"
    }

    conn = build_conn(:post, "/api/events", payload)

    assert {:ok, request} = Request.build(conn)
    assert request.query_params["foo"] == "bar"
    assert request.query_params["baz"] == "bam"
  end

  test "returns validation error when using data uris" do
    payload = %{
      name: "pageview",
      domain: "dummy.site",
      url: "data:text/html,%3Cscript%3Ealert%28%27hi%27%29%3B%3C%2Fscript%3E"
    }

    conn = build_conn(:post, "/api/events", payload)
    assert {:error, changeset} = Request.build(conn)
    assert {"scheme is not allowed", _} = changeset.errors[:url]
  end

  test "returns validation error when url is too long" do
    payload = %{
      name: "pageview",
      domain: "dummy.site",
      url: "https://dummy.site/#{String.duplicate("a", 5000)}"
    }

    conn = build_conn(:post, "/api/events", payload)
    assert {:error, changeset} = Request.build(conn)
    assert {"must be a valid url", _} = changeset.errors[:url]
  end

  test "returns validation error when event name is too long" do
    payload = %{
      name: String.duplicate("a", 500),
      domain: "dummy.site",
      url: "https://dummy.site/"
    }

    conn = build_conn(:post, "/api/events", payload)
    assert {:error, changeset} = Request.build(conn)
    assert {"should be at most %{count} character(s)", _} = changeset.errors[:event_name]
  end

  test "returns validation error when event name is blank" do
    payload = %{
      name: nil,
      domain: "dummy.site",
      url: "https://dummy.site/"
    }

    conn = build_conn(:post, "/api/events", payload)
    assert {:error, changeset} = Request.build(conn)
    assert {"can't be blank", _} = changeset.errors[:event_name]
  end

  test "returns validation error when event name cannot be cast to string" do
    payload = %{
      name: ["list", "of", "things"],
      domain: "dummy.site",
      url: "https://dummy.site/"
    }

    conn = build_conn(:post, "/api/events", payload)
    assert {:error, changeset} = Request.build(conn)
    assert {"is invalid", _} = changeset.errors[:event_name]
  end

  test "truncates referrer when too long" do
    payload = %{
      name: "pageview",
      domain: "dummy.site",
      url: "https://dummy.site/",
      referrer: String.duplicate("a", 2500)
    }

    conn = build_conn(:post, "/api/events", payload)
    assert {:ok, request} = Request.build(conn)
    assert request.referrer == String.duplicate("a", 2000)
  end

  test "returns validation error when props keys are too long" do
    payload = %{
      name: "pageview",
      domain: "dummy.site",
      url: "https://dummy.site/",
      props: %{String.duplicate("a", 500) => "abc"}
    }

    conn = build_conn(:post, "/api/events", payload)
    assert {:error, changeset} = Request.build(conn)

    assert {"keys should have at most 300 bytes and values 2000 bytes", _} =
             changeset.errors[:props]
  end

  test "returns validation error when props values are too long" do
    payload = %{
      name: "pageview",
      domain: "dummy.site",
      url: "https://dummy.site/",
      props: %{"abc" => String.duplicate("a", 2500)}
    }

    conn = build_conn(:post, "/api/events", payload)
    assert {:error, changeset} = Request.build(conn)

    assert {"keys should have at most 300 bytes and values 2000 bytes", _} =
             changeset.errors[:props]
  end

  test "trims prop list to 30 items when sending too many items" do
    payload = %{
      name: "pageview",
      domain: "dummy.site",
      url: "http://dummy.site/index.html",
      referrer: "https://example.com",
      hashMode: 1,
      props: for(i <- 1..50, do: {"#{i}", "foo"}, into: %{})
    }

    conn = build_conn(:post, "/api/events", payload)

    assert {:ok, request} = Request.build(conn)
    assert map_size(request.props) == 30
  end

  test "malicious input, technically valid json" do
    conn = build_conn(:post, "/api/events", "\"<script>\"")
    assert {:error, changeset} = Request.build(conn)
    assert changeset.errors[:request]
  end

  @tag :ee_only
  test "encodable" do
    params = %{
      name: "pageview",
      domain: "dummy.site",
      url: "https://dummy.site/pictures/index.html?foo=bar&baz=bam",
      referrer: "https://example.com",
      props: %{"abc" => "qwerty", "hello" => "world"},
      hashMode: 1,
      revenue: %{
        "amount" => "12.3",
        "currency" => "USD"
      }
    }

    assert {:ok, request} =
             build_conn(:post, "/api/events", params)
             |> put_req_header("user-agent", "Mozilla")
             |> Request.build()

    request = request |> Jason.encode!() |> Jason.decode!()

    assert Map.drop(request, ["timestamp"]) == %{
             "domains" => ["dummy.site"],
             "event_name" => "pageview",
             "hash_mode" => 1,
             "hostname" => "dummy.site",
             "pathname" => "/pictures/index.html",
             "props" => %{"abc" => "qwerty", "hello" => "world"},
             "query_params" => %{"baz" => "bam", "foo" => "bar"},
             "referrer" => "https://example.com",
             "remote_ip" => "127.0.0.1",
             "revenue_source" => %{"amount" => "12.3", "currency" => "USD"},
             "uri" => "https://dummy.site/pictures/index.html?foo=bar&baz=bam",
             "user_agent" => "Mozilla",
             "ip_classification" => nil
           }

    assert %NaiveDateTime{} = NaiveDateTime.from_iso8601!(request["timestamp"])
  end
end
