defmodule Phoenix.Sync.RouterTest do
  @pool_opts [backoff_type: :stop, max_restarts: 0, pool_size: 2]

  use ExUnit.Case,
    async: false,
    parameterize: [
      %{
        sync_config: [
          env: :test,
          mode: :embedded,
          pool_opts: @pool_opts
        ]
      },
      %{
        sync_config: [
          env: :test,
          mode: :http,
          url: "http://localhost:3000",
          pool_opts: @pool_opts
        ]
      }
    ]

  use Support.ElectricHelpers, endpoint: __MODULE__.Endpoint

  import Plug.Test

  require Phoenix.ConnTest

  defmodule Router do
    use Phoenix.Router

    import Phoenix.Sync.Router

    scope "/sync" do
      # by default we take the table name from the path
      # note that this does not handle weird table names that need quoting
      # or namespaces
      sync "/todos", Support.Todo

      # or we can expliclty specify the table
      sync "/things-to-do", table: "todos"

      # to use a non-standard namespace, we include it
      # so in this case the table is "food"."toeats"
      sync "/toeats", table: "toeats", namespace: "food"

      # or we can expliclty specify the table
      sync "/ideas",
        table: "ideas",
        where: "plausible = true",
        columns: ["id", "title"],
        replica: :full,
        storage: %{compaction: :disabled}

      # support shapes from a query, passed as the 2nd arg
      sync "/query-where", Support.Todo, where: "completed = false"

      sync "/shape-parameters", table: "todos", where: "completed = $1", params: ["false"]
      sync "/query-parameters", Support.Todo, where: "completed = $1", params: ["false"]

      # or as query: ...
      sync "/query-bare", Support.Todo

      # query version also accepts shape config
      sync "/query-config", Support.Todo, replica: :full
      sync "/query-config2", Support.Todo, replica: :full, storage: %{compaction: :disabled}

      sync "/typo", Support.Todoo

      # TODO: see if there's a way to pass captures through
      # sync "/map/query-capture", Support.Todo, transform: &Phoenix.Sync.RouterTest.map_todo/1
      # sync "/map/keyword-capture", table: "todos", transform: &Phoenix.Sync.RouterTest.map_todo/1

      sync "/map/query-mfa", Support.Todo,
        transform: {Phoenix.Sync.RouterTest, :map_todo, ["query-mfa"]}

      sync "/map/keyword-mfa",
        table: "todos",
        transform: {Phoenix.Sync.RouterTest, :map_todo, ["keyword-mfa"]}

      sync "/map/ecto-schema", Support.Organization, transform: Support.Organization
    end

    scope "/namespaced-sync", WebNamespace do
      sync "/todos", Support.Todo
    end
  end

  def map_todo(
        %{"headers" => %{"operation" => op}} = msg,
        route \\ "capture"
      ) do
    Map.update!(msg, "value", fn value ->
      Map.put(value, "merged", "#{route}-#{op}-#{value["id"]}-#{value["title"]}")
    end)
  end

  defmodule Endpoint do
    use Phoenix.Endpoint, otp_app: :phoenix_sync

    plug Router
  end

  Code.ensure_loaded!(Support.Todo)
  Code.ensure_loaded!(Support.Organization)
  Code.ensure_loaded!(Support.Repo)

  @organizations [
    table: {
      "organizations",
      [
        "id int8 not null primary key generated always as identity",
        "name text",
        "external_id uuid",
        "address jsonb",
        "inserted_at timestamp with time zone",
        "updated_at timestamp with time zone"
      ]
    },
    data: {
      "organizations",
      ["name", "external_id", "address", "inserted_at", "updated_at"],
      [
        [
          "one",
          Ecto.UUID.dump!("dfca7ea8-f0d0-47cf-984d-d170f6b989d3"),
          %{id: "a8a2cd54-f892-449f-8a1b-1a4a88034bf3", street: "High Street", number: 12},
          ~U[2025-01-01T12:34:14Z],
          ~U[2025-01-01T12:34:14Z]
        ],
        [
          "two",
          Ecto.UUID.dump!("017fad69-0603-4dd5-811f-b4f83f45e7af"),
          %{id: "82f22f21-8526-4976-84b2-093ab905394d", street: "Market Street", number: 3},
          ~U[2025-01-02T12:34:14Z],
          ~U[2025-01-02T12:34:14Z]
        ]
      ]
    }
  ]

  setup [
    :define_endpoint,
    :with_stack_id_from_test,
    :with_unique_db,
    :with_stack_config,
    :with_table,
    :with_data,
    :start_embedded,
    :configure_endpoint
  ]

  describe "Phoenix.Router - sync/2" do
    @tag table: {
           "todos",
           [
             "id int8 not null primary key generated always as identity",
             "title text",
             "completed boolean default false"
           ]
         }
    @tag data: {"todos", ["title"], [["one"], ["two"], ["three"]]}

    test "supports schema modules", _ctx do
      resp =
        Phoenix.ConnTest.build_conn()
        |> Phoenix.ConnTest.get("/sync/todos", %{offset: "-1"})

      assert resp.status == 200
      assert Plug.Conn.get_resp_header(resp, "electric-offset") == ["0_0"]

      assert [
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "one"}},
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "two"}},
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "three"}}
             ] = Jason.decode!(resp.resp_body)
    end

    @tag table: {
           "todos",
           [
             "id int8 not null primary key generated always as identity",
             "title text",
             "completed boolean default false"
           ]
         }
    @tag data: {"todos", ["title"], [["one"], ["two"], ["three"]]}
    test "supports schema modules within aliased scope", _ctx do
      resp =
        Phoenix.ConnTest.build_conn()
        |> Phoenix.ConnTest.get("/namespaced-sync/todos", %{offset: "-1"})

      assert resp.status == 200
      assert Plug.Conn.get_resp_header(resp, "electric-offset") == ["0_0"]

      assert [
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "one"}},
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "two"}},
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "three"}}
             ] = Jason.decode!(resp.resp_body)
    end

    @tag table: {
           "todos",
           [
             "id int8 not null primary key generated always as identity",
             "title text",
             "completed boolean default false"
           ]
         }
    @tag data: {"todos", ["title"], [["one"], ["two"], ["three"]]}

    test "allows for specifying the table explicitly", _ctx do
      resp =
        Phoenix.ConnTest.build_conn()
        |> Phoenix.ConnTest.get("/sync/things-to-do", %{offset: "-1"})

      assert resp.status == 200
      assert Plug.Conn.get_resp_header(resp, "electric-offset") == ["0_0"]

      assert [
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "one"}},
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "two"}},
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "three"}}
             ] = Jason.decode!(resp.resp_body)
    end

    @tag table: {
           "todos",
           [
             "id int8 not null primary key generated always as identity",
             "title text",
             "completed boolean default false"
           ]
         }
    @tag data: {"todos", ["title"], [["one"], ["two"], ["three"]]}

    test "returns a correct content-type header", _ctx do
      resp =
        Phoenix.ConnTest.build_conn()
        |> Phoenix.ConnTest.get("/sync/things-to-do", %{offset: "-1"})

      assert resp.status == 200
      assert Plug.Conn.get_resp_header(resp, "electric-offset") == ["0_0"]

      assert Plug.Conn.get_resp_header(resp, "content-type") == [
               "application/json; charset=utf-8"
             ]
    end

    @tag table: {
           "todos",
           [
             "id int8 not null primary key generated always as identity",
             "title text",
             "completed boolean default false"
           ]
         }
    @tag data: {"todos", ["title"], [["one"], ["two"], ["three"]]}
    test "returns CORS headers", _ctx do
      resp =
        Phoenix.ConnTest.build_conn()
        |> Phoenix.ConnTest.get("/sync/things-to-do", %{offset: "-1"})

      assert resp.status == 200
      assert [expose] = Plug.Conn.get_resp_header(resp, "access-control-expose-headers")
      assert String.contains?(expose, "electric-offset")
    end

    @tag table: {
           "ideas",
           [
             "id int8 not null primary key generated always as identity",
             "title text",
             "plausible boolean default false",
             "completed boolean default false"
           ]
         }
    @tag data: {
           "ideas",
           ["title", "plausible"],
           [["world peace", false], ["world war", true], ["make tea", true]]
         }

    test "allows for mixed definition using path and [where, column] modifiers", _ctx do
      resp =
        Phoenix.ConnTest.build_conn()
        |> Phoenix.ConnTest.get("/sync/ideas", %{offset: "-1"})

      assert resp.status == 200
      assert Plug.Conn.get_resp_header(resp, "electric-offset") == ["0_0"]

      assert [
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "world war"}},
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "make tea"}}
             ] = Jason.decode!(resp.resp_body)
    end

    @tag table: {
           {"food", "toeats"},
           [
             "id int8 not null primary key generated always as identity",
             "food text"
           ]
         }
    @tag data: {
           {"food", "toeats"},
           ["food"],
           [["peas"], ["beans"], ["sweetcorn"]]
         }

    test "can provide a custom namespace", _ctx do
      resp =
        Phoenix.ConnTest.build_conn()
        |> Phoenix.ConnTest.get("/sync/toeats", %{offset: "-1"})

      assert resp.status == 200
      assert Plug.Conn.get_resp_header(resp, "electric-offset") == ["0_0"]

      assert [
               %{"headers" => %{"operation" => "insert"}, "value" => %{"food" => "peas"}},
               %{"headers" => %{"operation" => "insert"}, "value" => %{"food" => "beans"}},
               %{"headers" => %{"operation" => "insert"}, "value" => %{"food" => "sweetcorn"}}
             ] = Jason.decode!(resp.resp_body)
    end

    @tag table: {
           "todos",
           [
             "id int8 not null primary key generated always as identity",
             "title text",
             "completed boolean default false"
           ]
         }
    @tag data: {
           "todos",
           ["title", "completed"],
           [["one", false], ["two", false], ["three", true]]
         }

    test "accepts Ecto queries as the shape definition", _ctx do
      resp =
        Phoenix.ConnTest.build_conn()
        |> Phoenix.ConnTest.get("/sync/query-where", %{offset: "-1"})

      assert resp.status == 200
      assert Plug.Conn.get_resp_header(resp, "electric-offset") == ["0_0"]

      assert [
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "one"}},
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "two"}}
             ] = Jason.decode!(resp.resp_body)

      resp =
        Phoenix.ConnTest.build_conn()
        |> Phoenix.ConnTest.get("/sync/query-bare", %{offset: "-1"})

      assert resp.status == 200
      assert Plug.Conn.get_resp_header(resp, "electric-offset") == ["0_0"]

      assert [
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "one"}},
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "two"}},
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "three"}}
             ] = Jason.decode!(resp.resp_body)

      resp =
        Phoenix.ConnTest.build_conn()
        |> Phoenix.ConnTest.get("/sync/query-config", %{offset: "-1"})

      assert resp.status == 200
      assert Plug.Conn.get_resp_header(resp, "electric-offset") == ["0_0"]

      assert [
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "one"}},
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "two"}},
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "three"}}
             ] = Jason.decode!(resp.resp_body)

      resp =
        Phoenix.ConnTest.build_conn()
        |> Phoenix.ConnTest.get("/sync/query-config2", %{offset: "-1"})

      assert resp.status == 200
      assert Plug.Conn.get_resp_header(resp, "electric-offset") == ["0_0"]

      assert [
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "one"}},
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "two"}},
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "three"}}
             ] = Jason.decode!(resp.resp_body)
    end

    @tag table: {
           "todos",
           [
             "id int8 not null primary key generated always as identity",
             "title text",
             "completed boolean default false"
           ]
         }
    @tag data: {
           "todos",
           ["title", "completed"],
           [["one", false], ["two", false], ["three", true]]
         }
    test "accepts parameterized where clauses", _ctx do
      resp =
        Phoenix.ConnTest.build_conn()
        |> Phoenix.ConnTest.get("/sync/shape-parameters", %{offset: "-1"})

      assert resp.status == 200
      assert Plug.Conn.get_resp_header(resp, "electric-offset") == ["0_0"]

      assert [
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "one"}},
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "two"}}
             ] = Jason.decode!(resp.resp_body)

      resp =
        Phoenix.ConnTest.build_conn()
        |> Phoenix.ConnTest.get("/sync/query-parameters", %{offset: "-1"})

      assert resp.status == 200
      assert Plug.Conn.get_resp_header(resp, "electric-offset") == ["0_0"]

      assert [
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "one"}},
               %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "two"}}
             ] = Jason.decode!(resp.resp_body)
    end

    @tag table: {
           "todos",
           [
             "id int8 not null primary key generated always as identity",
             "title text",
             "completed boolean default false"
           ]
         }
    @tag data: {
           "todos",
           ["title", "completed"],
           [["one", false], ["two", false], ["three", true]]
         }
    test "cursor header is correctly returned", _ctx do
      resp =
        Phoenix.ConnTest.build_conn()
        |> Phoenix.ConnTest.get("/sync/query-where", %{offset: "-1"})

      assert [handle] = Plug.Conn.get_resp_header(resp, "electric-handle")
      assert [offset] = Plug.Conn.get_resp_header(resp, "electric-offset")

      resp =
        Phoenix.ConnTest.build_conn()
        |> Phoenix.ConnTest.get("/sync/query-where", %{
          offset: offset,
          handle: handle,
          live: false
        })

      assert [handle] = Plug.Conn.get_resp_header(resp, "electric-handle")
      assert [offset] = Plug.Conn.get_resp_header(resp, "electric-offset")
      assert [_] = Plug.Conn.get_resp_header(resp, "electric-up-to-date")

      resp =
        Phoenix.ConnTest.build_conn()
        |> Phoenix.ConnTest.get("/sync/query-where", %{
          offset: offset,
          handle: handle,
          live: true,
          cursor: ""
        })

      assert ["0"] = Plug.Conn.get_resp_header(resp, "electric-cursor")
    end

    test "raises clear error error if the schema module is not found" do
      assert_raise ArgumentError,
                   ~r/Invalid query `Support.Todoo`: the given module does not exist/,
                   fn ->
                     Phoenix.ConnTest.build_conn()
                     |> Phoenix.ConnTest.get("/sync/typo", %{offset: "-1"})
                   end
    end

    @tag table: {
           "todos",
           [
             "id int8 not null primary key generated always as identity",
             "title text",
             "completed boolean default false"
           ]
         }
    @tag data: {
           "todos",
           ["title", "completed"],
           [["one", false], ["two", false]]
         }

    @tag transform: true
    test "mapping values" do
      resp =
        Phoenix.ConnTest.build_conn()
        |> Phoenix.ConnTest.get("/sync/map/query-mfa", %{offset: "-1"})

      assert resp.status == 200
      assert Plug.Conn.get_resp_header(resp, "electric-offset") == ["0_0"]

      assert [
               %{
                 "headers" => %{"operation" => "insert"},
                 "value" => %{"merged" => "query-mfa-insert-1-one", "title" => "one"}
               },
               %{
                 "headers" => %{"operation" => "insert"},
                 "value" => %{"merged" => "query-mfa-insert-2-two", "title" => "two"}
               }
             ] = Jason.decode!(resp.resp_body)
    end

    @tag transform: true
    test "captures in transform are not supported in phoenix.router" do
      assert_raise ArgumentError,
                   ~r/Invalid transform function specification in sync shape definition\. When using Phoenix Router please use an MFA tuple \(`transform: \{Mod, :fun, \[arg1, \.\.\.\]\}`\)/,
                   fn ->
                     Code.compile_string("""
                     defmodule InvalidCaptureRouter#{System.unique_integer([:positive, :monotonic])} do
                       use Phoenix.Router

                       import Phoenix.Sync.Router

                       scope "/sync" do
                         sync "/map/query-capture", Support.Todo, transform: &Phoenix.Sync.RouterTest.map_todo/1
                       end
                     end
                     """)
                   end
    end

    @tag @organizations
    @tag transform: true
    test "transform via ecto schema modules" do
      resp =
        Phoenix.ConnTest.build_conn()
        |> Phoenix.ConnTest.get("/sync/map/ecto-schema", %{offset: "-1"})

      assert resp.status == 200
      assert Plug.Conn.get_resp_header(resp, "electric-offset") == ["0_0"]

      assert [
               %{
                 "headers" => %{"operation" => "insert"},
                 "value" => %{
                   "external_id" => "org_37fh5khq2bd47gcn2fypnomj2m",
                   "name" => "one",
                   "address" => %{
                     "id" => "a8a2cd54-f892-449f-8a1b-1a4a88034bf3",
                     "number" => 12,
                     "street" => "High Street"
                   },
                   "id" => 1,
                   "inserted_at" => "2025-01-01T12:34:14",
                   "updated_at" => "2025-01-01T12:34:14"
                 }
               },
               %{
                 "headers" => %{"operation" => "insert"},
                 "value" => %{
                   "external_id" => "org_af7222igang5lai7wt4d6rphv4",
                   "name" => "two",
                   "address" => %{
                     "id" => "82f22f21-8526-4976-84b2-093ab905394d",
                     "number" => 3,
                     "street" => "Market Street"
                   },
                   "id" => 2,
                   "inserted_at" => "2025-01-02T12:34:14",
                   "updated_at" => "2025-01-02T12:34:14"
                 }
               }
             ] = Jason.decode!(resp.resp_body)
    end
  end

  describe "Plug.Router - shape/2" do
    @describetag table: {
                   "todos",
                   [
                     "id int8 not null primary key generated always as identity",
                     "title text",
                     "completed boolean default false",
                     "plausible boolean default false"
                   ]
                 }
    @describetag data: {
                   "todos",
                   ["title", "plausible"],
                   [["one", true], ["two", true], ["three", true]]
                 }

    defmodule MyScope do
      use Plug.Router
      use Phoenix.Sync.Router, opts_in_assign: :options

      plug :match
      plug :dispatch

      sync "/todos", Support.Todo
    end

    defmodule MyRouter do
      use Plug.Router, copy_opts_to_assign: :options
      use Phoenix.Sync.Router

      plug :match
      plug :dispatch

      get "/" do
        send_resp(conn, 200, "hello")
      end

      sync "/shapes/todos", Support.Todo
      sync "/shapes/things-to-do", table: "todos"

      sync "/shapes/ideas",
        table: "todos",
        where: "plausible = true",
        columns: ["id", "title"],
        replica: :full,
        storage: %{compaction: :disabled}

      sync "/shapes/query-module", Support.Todo, where: "completed = false"

      sync "/shapes/map-module", Support.Todo,
        where: "completed = false",
        transform: {Phoenix.Sync.RouterTest, :map_todo, ["module-mfa"]}

      sync "/shapes/map-capture", Support.Todo,
        where: "completed = false",
        transform: &Phoenix.Sync.RouterTest.map_todo/1

      sync "/shapes/map-ecto", Support.Organization, transform: Support.Organization

      forward "/namespace", to: MyScope

      match _ do
        send_resp(conn, 404, "not found")
      end
    end

    setup(ctx) do
      [plug_opts: [phoenix_sync: Phoenix.Sync.plug_opts(ctx.electric_opts)]]
    end

    test "raises compile-time error if Plug.Router is not configured to copy_opts_to_assign" do
      assert_raise ArgumentError, fn ->
        Code.compile_string("""
        defmodule BreakingRouter#{System.unique_integer([:positive, :monotonic])} do
          use Plug.Router
          use Phoenix.Sync.Router

          plug :match
          plug :dispatch

          sync "/shapes/todos", Support.Todo
        end
        """)
      end
    end

    test "doesn't raise compile time error if copy_opts_to_assign is set in the opts" do
      Code.compile_string("""
      defmodule WorkingRouter#{System.unique_integer([:positive, :monotonic])} do
        use Plug.Router
        use Phoenix.Sync.Router, opts_in_assign: :options

        plug :match
        plug :dispatch

        sync "/todos", Support.Todo
      end
      """)
    end

    for path <-
          ~w(/shapes/todos /shapes/things-to-do /shapes/ideas /shapes/query-module /namespace/todos) do
      test "plug route #{path}", ctx do
        resp =
          conn(:get, unquote(path), %{"offset" => "-1"})
          |> MyRouter.call(ctx.plug_opts)

        assert resp.status == 200
        assert Plug.Conn.get_resp_header(resp, "electric-offset") == ["0_0"]

        assert [
                 %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "one"}},
                 %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "two"}},
                 %{"headers" => %{"operation" => "insert"}, "value" => %{"title" => "three"}}
               ] = Jason.decode!(resp.resp_body)
      end
    end

    @tag transform: true
    test "response mapping", ctx do
      resp =
        conn(:get, "/shapes/map-module", %{"offset" => "-1"})
        |> MyRouter.call(ctx.plug_opts)

      assert resp.status == 200
      assert Plug.Conn.get_resp_header(resp, "electric-offset") == ["0_0"]

      assert [
               %{
                 "headers" => %{"operation" => "insert"},
                 "value" => %{"merged" => "module-mfa-insert-1-one", "title" => "one"}
               },
               %{
                 "headers" => %{"operation" => "insert"},
                 "value" => %{"merged" => "module-mfa-insert-2-two", "title" => "two"}
               },
               %{
                 "headers" => %{"operation" => "insert"},
                 "value" => %{"merged" => "module-mfa-insert-3-three", "title" => "three"}
               }
             ] = Jason.decode!(resp.resp_body)
    end

    @tag transform: true
    test "response mapping with capture", ctx do
      resp =
        conn(:get, "/shapes/map-capture", %{"offset" => "-1"})
        |> MyRouter.call(ctx.plug_opts)

      assert resp.status == 200
      assert Plug.Conn.get_resp_header(resp, "electric-offset") == ["0_0"]

      assert [
               %{
                 "headers" => %{"operation" => "insert"},
                 "value" => %{"merged" => "capture-insert-1-one", "title" => "one"}
               },
               %{
                 "headers" => %{"operation" => "insert"},
                 "value" => %{"merged" => "capture-insert-2-two", "title" => "two"}
               },
               %{
                 "headers" => %{"operation" => "insert"},
                 "value" => %{"merged" => "capture-insert-3-three", "title" => "three"}
               }
             ] = Jason.decode!(resp.resp_body)
    end

    @tag transform: true
    test "only &module.fun/1 style captures are accepted" do
      assert_raise ArgumentError,
                   ~r/Invalid transform function specification in sync shape definition/,
                   fn ->
                     Code.compile_string("""
                     defmodule InvalidCaptureRouter#{System.unique_integer([:positive, :monotonic])} do
                       use Plug.Router, copy_opts_to_assign: :options
                       use Phoenix.Sync.Router

                       plug :match
                       plug :dispatch

                       sync "/shapes/todos", Support.Todo,
                         transform: &#{inspect(__MODULE__)}.map_todo(&1, "invalid")
                     end
                     """)
                   end
    end

    @tag @organizations
    @tag transform: true
    test "ecto schema modules are valid transforms", ctx do
      resp =
        conn(:get, "/shapes/map-ecto", %{"offset" => "-1"})
        |> MyRouter.call(ctx.plug_opts)

      assert resp.status == 200
      assert Plug.Conn.get_resp_header(resp, "electric-offset") == ["0_0"]

      assert [
               %{
                 "headers" => %{"operation" => "insert"},
                 "value" => %{
                   "external_id" => "org_37fh5khq2bd47gcn2fypnomj2m",
                   "name" => "one",
                   "address" => %{
                     "id" => "a8a2cd54-f892-449f-8a1b-1a4a88034bf3",
                     "number" => 12,
                     "street" => "High Street"
                   },
                   "id" => 1,
                   "inserted_at" => "2025-01-01T12:34:14",
                   "updated_at" => "2025-01-01T12:34:14"
                 }
               },
               %{
                 "headers" => %{"operation" => "insert"},
                 "value" => %{
                   "external_id" => "org_af7222igang5lai7wt4d6rphv4",
                   "name" => "two",
                   "address" => %{
                     "id" => "82f22f21-8526-4976-84b2-093ab905394d",
                     "number" => 3,
                     "street" => "Market Street"
                   },
                   "id" => 2,
                   "inserted_at" => "2025-01-02T12:34:14",
                   "updated_at" => "2025-01-02T12:34:14"
                 }
               }
             ] = Jason.decode!(resp.resp_body)
    end

    test "returns CORS headers", ctx do
      resp =
        conn(:get, "/shapes/todos", %{"offset" => "-1"})
        |> MyRouter.call(ctx.plug_opts)

      assert resp.status == 200
      assert [expose] = Plug.Conn.get_resp_header(resp, "access-control-expose-headers")
      assert String.contains?(expose, "electric-offset")
    end
  end
end
