module Feature.Query.ComputedRelsSpec where

import Network.Wai (Application)

import Network.HTTP.Types
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON

import Protolude  hiding (get)
import SpecHelper

spec :: SpecWith ((), Application)
spec = describe "computed relationships" $ do
  it "can define a many-to-one relationship with SETOF and ROWS 1 and embed" $
    get "/videogames?select=name,designer:computed_designers(name)"
    `shouldRespondWith`
      [json|[
        {"name":"Civilization I","designer":{"name":"Sid Meier"}},
        {"name":"Civilization II","designer":{"name":"Sid Meier"}},
        {"name":"Final Fantasy I","designer":{"name":"Hironobu Sakaguchi"}},
        {"name":"Final Fantasy II","designer":{"name":"Hironobu Sakaguchi"}}
      ]|] { matchHeaders = [matchContentTypeJson] }

  it "can define a many-to-one relationship without SETOF and embed" $
    get "/videogames?select=name,designer:computed_designers_noset(name)"
    `shouldRespondWith`
      [json|[
        {"name":"Civilization I","designer":{"name":"Sid Meier"}},
        {"name":"Civilization II","designer":{"name":"Sid Meier"}},
        {"name":"Final Fantasy I","designer":{"name":"Hironobu Sakaguchi"}},
        {"name":"Final Fantasy II","designer":{"name":"Hironobu Sakaguchi"}}
      ]|] { matchHeaders = [matchContentTypeJson] }

  it "can define a one-to-many relationship and embed" $
    get "/designers?select=name,videogames:computed_videogames(name)"
    `shouldRespondWith`
      [json|[
        {"name":"Sid Meier","videogames":[{"name":"Civilization I"}, {"name":"Civilization II"}]},
        {"name":"Hironobu Sakaguchi","videogames":[{"name":"Final Fantasy I"}, {"name":"Final Fantasy II"}]}
      ]|] { matchHeaders = [matchContentTypeJson] }

  it "works with !inner and count=exact" $ do
    request methodGet "/designers?select=name,videogames:computed_videogames!inner(name)&videogames.name=eq.Civilization%20I"
      [("Prefer", "count=exact")] ""
      `shouldRespondWith`
        [json|[{"name":"Sid Meier","videogames":[{"name":"Civilization I"}]}]|]
        { matchStatus  = 200
        , matchHeaders = ["Content-Range" <:> "0-0/1"]
        }
    request methodGet "/videogames?select=name,designer:computed_designers!inner(name)&designer.name=like.*Hironobu*"
      [("Prefer", "count=exact")] ""
      `shouldRespondWith`
        [json|[
          {"name":"Final Fantasy I","designer":{"name":"Hironobu Sakaguchi"}},
          {"name":"Final Fantasy II","designer":{"name":"Hironobu Sakaguchi"}}
        ]|]
        { matchStatus  = 200
        , matchHeaders = ["Content-Range" <:> "0-1/2"]
        }
    request methodGet "/videogames?select=name,designer:computed_designers_noset!inner(name)&designer.name=like.*Hironobu*"
      [("Prefer", "count=exact")] ""
      `shouldRespondWith`
        [json|[
          {"name":"Final Fantasy I","designer":{"name":"Hironobu Sakaguchi"}},
          {"name":"Final Fantasy II","designer":{"name":"Hironobu Sakaguchi"}}
        ]|]
        { matchStatus  = 200
        , matchHeaders = ["Content-Range" <:> "0-1/2"]
        }

  it "works with rpc" $ do
    get "/rpc/getallvideogames?select=name,designer:computed_designers(name)"
      `shouldRespondWith`
      [json|[
        {"name":"Civilization I","designer":{"name":"Sid Meier"}},
        {"name":"Civilization II","designer":{"name":"Sid Meier"}},
        {"name":"Final Fantasy I","designer":{"name":"Hironobu Sakaguchi"}},
        {"name":"Final Fantasy II","designer":{"name":"Hironobu Sakaguchi"}}
      ]|] { matchHeaders = [matchContentTypeJson] }
    get "/rpc/getalldesigners?select=name,videogames:computed_videogames(name)"
      `shouldRespondWith`
      [json|[
        {"name":"Sid Meier","videogames":[{"name":"Civilization I"}, {"name":"Civilization II"}]},
        {"name":"Hironobu Sakaguchi","videogames":[{"name":"Final Fantasy I"}, {"name":"Final Fantasy II"}]}
      ]|] { matchHeaders = [matchContentTypeJson] }

  it "works with mutations" $ do
    request methodPost "/videogames?select=name,designer:computed_designers(name)"
      [("Prefer", "return=representation")]
      [json| {"id": 5, "name": "Chrono Trigger", "designer_id": 2} |]
      `shouldRespondWith`
        [json|[ {"name":"Chrono Trigger","designer":{"name":"Hironobu Sakaguchi"}} ]|]
        { matchStatus  = 201 }
    request methodPatch "/designers?select=name,videogames:computed_videogames(name)&id=eq.1"
      [("Prefer", "return=representation")]
      [json| {"name": "Sidney K. Meier"} |]
      `shouldRespondWith`
        [json|[ { "name": "Sidney K. Meier", "videogames": [{"name":"Civilization I"}, {"name":"Civilization II"}] } ]|]
        { matchStatus  = 200 }
    request methodDelete "/videogames?select=name,designer:computed_designers(name)&id=eq.3"
      [("Prefer", "return=representation")] ""
      `shouldRespondWith`
        [json|[ {"name":"Final Fantasy I","designer":{"name":"Hironobu Sakaguchi"}} ]|]
        { matchStatus  = 200 }

  it "applies data representations to response" $ do
    -- A smoke test for data reps in the presence of computed relations.

    -- The data rep here title cases the designer name before presentation. So here the lowercase version will be saved,
    -- but the title case version returned. Pulling in a computed relation should not confuse this.
    request methodPatch "/designers?select=name,videogames:computed_videogames(name)&id=eq.1"
      [("Prefer", "return=representation"), ("Prefer", "tx=commit")]
      [json| {"name": "sidney k. meier"} |]
      `shouldRespondWith`
      [json|[{"name":"Sidney K. Meier","videogames":[{"name":"Civilization I"}, {"name":"Civilization II"}]}]|]
      { matchStatus = 200 }

    -- Verify it was saved the way we requested (there's no text data rep for this column, so if we select with the wrong casing, it should fail.)
    get "/designers?select=id&name=eq.Sidney%20K.%20Meier"
      `shouldRespondWith`
      [json|[]|]
      { matchStatus = 200, matchHeaders = [matchContentTypeJson] }
    -- But with the right casing it works.
    get "/designers?select=id,name&name=eq.sidney%20k.%20meier"
      `shouldRespondWith`
      [json|[{"id": 1, "name":"Sidney K. Meier"}]|]
      { matchStatus = 200, matchHeaders = [matchContentTypeJson] }

    -- Most importantly, if you read it back even via a computed relation, the data rep should be applied.
    get "/videogames?select=name,designer:computed_designers(*)&id=eq.1"
      `shouldRespondWith`
      [json|[
        {"name":"Civilization I","designer":{"id": 1, "name":"Sidney K. Meier"}}
      ]|] { matchHeaders = [matchContentTypeJson] }

    -- reset the test fixture
    request methodPatch "/designers?id=eq.1"
      [("Prefer", "tx=commit")]
      [json| {"name": "Sid Meier"} |]
      `shouldRespondWith` 204
    -- need to poke the second one too to prevent inherent ordering from changing
    request methodPatch "/designers?id=eq.2"
      [("Prefer", "tx=commit")]
      [json| {"name": "Hironobu Sakaguchi"} |]
      `shouldRespondWith` 204

  it "works with self joins" $
    get "/web_content?select=name,child_web_content(name),parent_web_content(name)&id=in.(0,1)"
    `shouldRespondWith`
      [json|[
        {"name":"tardis","child_web_content":[{"name":"fezz"}, {"name":"foo"}, {"name":"bar"}],"parent_web_content":{"name":"wat"}},
        {"name":"fezz","child_web_content":[{"name":"wut"}],"parent_web_content":{"name":"tardis"}}
      ]|] { matchHeaders = [matchContentTypeJson] }

  it "can override many-to-one and one-to-many relationships" $ do
    get "/videogames?select=*,designers!inner(*)"
      `shouldRespondWith`
      [json|[]|] { matchHeaders = [matchContentTypeJson] }
    get "/designers?select=*,videogames!inner(*)"
      `shouldRespondWith`
      [json|[]|] { matchHeaders = [matchContentTypeJson] }

  it "can override one-to-one relationships(would give disambiguation errors otherwise)" $ do
    get "/first_1?select=*,second_1(*)"
      `shouldRespondWith`
      [json|[]|] { matchHeaders = [matchContentTypeJson] }
    get "/second_1?select=*,first_1(*)"
      `shouldRespondWith`
      [json|[]|] { matchHeaders = [matchContentTypeJson] }

  -- https://github.com/PostgREST/postgrest/issues/2455
  it "creates queries with the right aliasing" $ do
    get "/fee?select=*,jsbaz(*,janedoe(*))"
      `shouldRespondWith`
      [json|[]|] { matchHeaders = [matchContentTypeJson] }
    get "/fee?select=*,jsbaz(*,johnsmith(*, fee(*)))"
      `shouldRespondWith`
      [json|[]|] { matchHeaders = [matchContentTypeJson] }

  it "creates queries with the right aliasing when following a normal embed" $ do
    get "/projects?select=name,clients(name,computed_projects(name))&limit=1"
      `shouldRespondWith`
      [json|
        [{"name":"Windows 7","clients":{"name":"Microsoft","computed_projects":{"name":"Windows 7"}}}]
      |] { matchHeaders = [matchContentTypeJson] }
    get "/clients?select=name,projects(name,computed_clients(name))&limit=1"
      `shouldRespondWith`
      [json|[
        {"name":"Microsoft","projects":[
          {"name":"Windows 7","computed_clients":{"name":"Microsoft"}},
          {"name":"Windows 10","computed_clients":{"name":"Microsoft"}}
        ]}
      ]|] { matchHeaders = [matchContentTypeJson] }

  -- https://github.com/PostgREST/postgrest/issues/2963
  context "can be defined using overloaded functions" $ do
    it "tables" $ do
      get "/items?select=*,computed_rel_overload(*)&limit=1"
        `shouldRespondWith`
        [json|
          [{"id":1,"computed_rel_overload":[{"id":1}]}]
        |] { matchHeaders = [matchContentTypeJson] }
      get "/items2?select=*,computed_rel_overload(*)&limit=1"
        `shouldRespondWith`
        [json|
          [{"id":1,"computed_rel_overload":[{"id":1},{"id":2}]}]
        |] { matchHeaders = [matchContentTypeJson] }

    it "rpc" $ do
      get "/rpc/search?id=1&select=*,computed_rel_overload(*)"
        `shouldRespondWith`
        [json|
          [{"id":1,"computed_rel_overload":[{"id":1}]}]
        |] { matchHeaders = [matchContentTypeJson] }
      get "/rpc/search2?id=1&select=*,computed_rel_overload(*)"
        `shouldRespondWith`
        [json|
          [{"id":1,"computed_rel_overload":[{"id":1},{"id":2}]}]
        |] { matchHeaders = [matchContentTypeJson] }
