
open Seppo_lib
module A = Assrt

(* diaspora profile json e.g.

   https://pod.diaspora.software/u/hq
   https://pod.diaspora.software/people/7bca7c80311b01332d046c626dd55703
*)

let test_person () =
  let empty : As2_vocab.Types.person = {
    id                          = Uri.empty;
    inbox                       = Uri.empty;
    outbox                      = Uri.empty;
    followers                   = None;
    following                   = None;
    attachment                  = [];
    discoverable                = false;
    generator                   = None;
    icon                        = None;
    image                       = None;
    manually_approves_followers = false;
    name                        = None;
    name_map                    = [];
    preferred_username          = None;
    preferred_username_map      = [];
    public_key                  = {
      id                        = Uri.empty;
      owner                     = None;
      pem                       = "";
      signatureAlgorithm        = None;
    };
    published                   = Some Ptime.epoch;
    summary                     = None;
    summary_map                 = [];
    url                         = [];
  } in
  let base = (Uri.of_string "https://example.com/su/") in
  let context = As2_vocab.Constants.ActivityStreams.und in
  {empty with id=Uri.make ~path:"id/" ()}
  |> As2_vocab.Encode.person ~context ~base
  |> Ezjsonm.value_to_string ~minify:false
  |> Assrt.equals_string __LOC__ {|{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1",
    {
      "schema": "http://schema.org#",
      "PropertyValue": "schema:PropertyValue",
      "value": "schema:value",
      "@language": "und"
    }
  ],
  "type": "Person",
  "id": "https://example.com/su/id/",
  "inbox": "https://example.com/su/",
  "outbox": "https://example.com/su/",
  "publicKey": {
    "@context": [
      {
        "@language": null
      }
    ],
    "id": "https://example.com/su/",
    "publicKeyPem": ""
  },
  "published": "1970-01-01T00:00:00Z",
  "manuallyApprovesFollowers": false,
  "discoverable": false,
  "attachment": []
}|};
  let p = {|{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1",
    {
      "schema": "http://schema.org#",
      "PropertyValue": "schema:PropertyValue",
      "value": "schema:value",
      "@language": "und"
    }
  ],
  "type": "Person",
  "id": "https://example.com/su/id/",
  "inbox": "https://example.com/su/",
  "outbox": "https://example.com/su/",
  "publicKey": {
    "id": "https://example.com/su/",
    "owner": "https://example.com/su/",
    "publicKeyPem": ""
  },
  "published": "1970-01-01T00:00:00Z",
  "manuallyApprovesFollowers": false,
  "discoverable": false,
  "attachment": []
}|}
          |> Ezjsonm.value_from_string
          |> As2_vocab.Decode.person
          |> Result.get_ok in
  p.id
  |> Uri.to_string
  |> Assrt.equals_string __LOC__ "https://example.com/su/id/"

let test_actor_3rd () =
  Logr.info (fun m -> m "%s.%s" "As2_vocab" "actor_3rd");
  let ok loc fn na =
    let j = File.in_channel fn Ezjsonm.from_channel in
    let p = j |> As2_vocab.Decode.person |> Result.get_ok in
    p.name |> Option.get |> A.equals_string loc na
  and err loc fn e =
    let j = File.in_channel fn Ezjsonm.from_channel in
    j |> As2_vocab.Decode.person |> Result.get_error
    |> Decoders_ezjsonm.Decode.string_of_error
    |> A.equals_string loc e
  and fal _loc fn =
    let j = File.in_channel fn Ezjsonm.from_channel in
    assert (j |> As2_vocab.Decode.person |> Result.is_error)
  in
  (* ok __LOC__ "data/ap/actor/lemmy.0.json" ""; *)
  ok __LOC__ "data/ap/actor/akkoma.0.json" "Sean Tilley";
  ok __LOC__ "data/ap/actor/akkoma.1.json" "Kinetix";
  ok __LOC__ "data/ap/actor/bonfire.0.json" "stpaultim";
  ok __LOC__ "data/ap/actor/bridgy.0.json" "Tantek Çelik";
  ok __LOC__ "data/ap/actor/friendica.0.json" "Michael Vogel";
  ok __LOC__ "data/ap/actor/gnusocial.0.json" "Diogo Peralta Cordeiro";
  ok __LOC__ "data/ap/actor/gnusocial.1.json" "admin de gnusocial.net";
  ok __LOC__ "data/ap/actor/gnusocial.2.json" "diogo";
  ok __LOC__ "data/ap/actor/gotosocial.0.json" "Gerben";
  ok __LOC__ "data/ap/actor/honk.0.json" "boyter";
  ok __LOC__ "data/ap/actor/mastodon.0.json" "Yet Another #Seppo! 🌻";
  ok __LOC__ "data/ap/actor/mastodon.1.json" "#Seppo";
  ok __LOC__ "data/ap/actor/mastodon.2.json" "Marcus Rohrmoser 🌍";
  ok __LOC__ "data/ap/actor/mini.0.json" "Yet Another #Seppo! 🌻";
  ok __LOC__ "data/ap/actor/misskey.0.json" "しゅいろ";
  ok __LOC__ "data/ap/actor/peertube.0.json" "edps";
  ok __LOC__ "data/ap/actor/peertube.2.json" {|edps|};
  ok __LOC__ "data/ap/actor/pixelfed.0.json" {|#Seppo|};
  ok __LOC__ "data/ap/actor/pleroma.0.json" "@fediverse@mro.name";
  ok __LOC__ "data/ap/actor/smithereen.0.json" {|Григорий Клюшников|};
  ok __LOC__ "data/ap/actor/snac.0.json" {|The Real Grunfink|};
  ok __LOC__ "data/ap/actor/threads.0.json" "Ben Savage";
  ok __LOC__ "data/ap/actor/tootik.0.json" {|EOIN GAIRLEOG|};
  ok __LOC__ "data/ap/actor/zap.0.json" "mike";
  ok __LOC__ "data/ap/profile/@actapopuli@fediverse.blog.json" "actapopuli";
  ok __LOC__ "data/ap/profile/@administrator@gnusocial.net.json" {|admin de gnusocial.net|};
  ok __LOC__ "data/ap/profile/@dansup@pixelfed.social.json" {|dansup|};
  ok __LOC__ "data/ap/profile/@gargron@mastodon.social.json" {|Eugen Rochko|};
  ok __LOC__ "data/ap/profile/@kainoa@calckey.social.json" {|Kainoa |};
  ok __LOC__ "data/ap/profile/@karolat@stereophonic.space.json" {|karolat|};
  ok __LOC__ "data/ap/profile/@manton@manton.org.json" {|Manton Reece|};
  ok __LOC__ "data/ap/profile/@matt@write.as.json" {|Matt|};
  ok __LOC__ "data/ap/profile/@mike@macgirvin.com.json" {|Mike Macgirvin|};
  ok __LOC__ "data/ap/profile/@peertube@framapiaf.org.json" {|PeerTube|};
  ok __LOC__ "data/ap/profile/@syuilo@misskey.io.json" {|:peroro_sama:しゅいろ:peroro_sama:|};
  ok __LOC__ "data/ap/profile/@tobias@friendi.ca.json" {|Tobias|};
  err __LOC__ "data/ap/profile/@lemmy_support@lemmy.ml.json" {|I tried the following decoders but they all failed:
  "type" decoder:
    in field "type": expected Person (received Group), but got "Group" 
  "type" decoder:
    in field "type": expected Service (received Group), but got "Group"  |};
  err __LOC__ "data/ap/actor/mobilizon.0.json" {|I tried the following decoders but they all failed:
  "type" decoder:
    in field "type": expected Person (received Group), but got "Group" 
  "type" decoder:
    in field "type": expected Service (received Group), but got "Group"  |};
  err __LOC__ "data/ap/actor/peertube.1.json" {|in field "summary": Expected a string, but got null|};
  fal __LOC__ "data/ap/actor/diaspora.0.json";
  ()
(*
  ok __LOC__ "data/ap/actor/natur.0.json" "しゅいろ";
  ok __LOC__ "data/ap/actor/natur.1.json" "しゅいろ";
  ok __LOC__ "data/ap/actor/sharkey.0.json" {||};
  ok __LOC__ "data/ap/actor/sharkey.1.json" {||};

 ok __LOC__ "data/ap/profile/@framasoft@mobilizon.fr.json" {||};
 ok __LOC__ "data/ap/profile/@Greensky@open.audio.json" {||};
*)

let test_note_decode () =
  Logr.info (fun m -> m "%s.%s" "As2_vocab" "note_decode");
  let j = File.in_channel "data/ap/note/mastodon.json" Ezjsonm.from_channel in
  let n = j |> As2_vocab.Decode.note |> Result.get_ok in
  n.id |> Uri.to_string |> Assrt.equals_string __LOC__ "https://digitalcourage.social/users/mro/statuses/111403080326863922";
  match n.in_reply_to with
  | [u] -> u |> Uri.to_string |> Assrt.equals_string __LOC__ "https://chaos.social/users/qyliss/statuses/111403054651938519"
  | _ -> failwith "none"

let test_webfinger_sunshine () =
  let q = File.in_channel "data/webfinger/mini.json" Ezjsonm.from_channel
          |> As2_vocab.Activitypub.Decode.Webfinger.query_result
          |> Result.get_ok in
  q.subject |> Assrt.equals_string __LOC__ "acct:ursi@example.com";
  q.links |> List.length |> Assrt.equals_int __LOC__ 3;
  let q = File.in_channel "data/webfinger/zap.json" Ezjsonm.from_channel
          |> As2_vocab.Activitypub.Decode.Webfinger.query_result
          |> Result.get_ok in
  q.subject |> Assrt.equals_string __LOC__ "acct:mike@macgirvin.com";
  q.links |> List.length |> Assrt.equals_int __LOC__ 3;
  let q = File.in_channel "data/webfinger/atom.json" Ezjsonm.from_channel
          |> As2_vocab.Activitypub.Decode.Webfinger.query_result
          |> Result.get_ok in
  q.subject |> Assrt.equals_string __LOC__ "acct:ursi@example.com";
  q.links |> List.length |> Assrt.equals_int __LOC__ 3

let test_profile_sunshine () =
  let q = File.in_channel "data/ap/actor/mini.0.json" Ezjsonm.from_channel
          |> As2_vocab.Activitypub.Decode.person
          |> Result.get_ok in
  q.name |> Option.get |> Assrt.equals_string __LOC__ "Yet Another #Seppo! 🌻";
  q.preferred_username |> Option.get |> Assrt.equals_string __LOC__ "ursi";
  q.attachment |> List.length |> Assrt.equals_int "" 0;
  let q = File.in_channel "data/ap/actor/mastodon.0.json" Ezjsonm.from_channel
          |> As2_vocab.Activitypub.Decode.person
          |> Result.get_ok in
  (match q.attachment with
   | [a;b;_] ->
     a.name |> Assrt.equals_string __LOC__ "Support";
     b.value |> Assrt.equals_string __LOC__ {|<a href="https://seppo.social">Seppo.Social</a>|};
   | _ -> Assrt.equals_int "" 0 1
  );
  q.image |> Option.get |> Uri.to_string |> Assrt.equals_string __LOC__ "https://example.com/me-banner.jpg";
  "" |> Assrt.equals_string __LOC__ "";
  let q = File.in_channel "data/ap/actor/akkoma.0.json" Ezjsonm.from_channel
          |> As2_vocab.Activitypub.Decode.person
          |> Result.get_ok in
  q.name |> Option.get |> Assrt.equals_string __LOC__ "Sean Tilley";
  let q = File.in_channel "data/ap/actor/gnusocial.0.json" Ezjsonm.from_channel
          |> As2_vocab.Activitypub.Decode.person
          |> Result.get_ok in
  q.preferred_username |> Option.get |> Assrt.equals_string __LOC__ "diogo";
  let q = File.in_channel "data/ap/actor/lemmy.0.json" Ezjsonm.from_channel
          |> As2_vocab.Activitypub.Decode.person
          |> Result.get_ok in
  q.preferred_username |> Option.get |> Assrt.equals_string __LOC__ "nutomic";
  let q = File.in_channel "data/ap/actor/mastodon.0.json" Ezjsonm.from_channel
          |> As2_vocab.Activitypub.Decode.person
          |> Result.get_ok in
  q.preferred_username |> Option.get |> Assrt.equals_string __LOC__ "ursi";
  let q = File.in_channel "data/ap/actor/peertube.0.json" Ezjsonm.from_channel
          |> As2_vocab.Activitypub.Decode.person
          |> Result.get_ok in
  q.preferred_username |> Option.get |> Assrt.equals_string __LOC__ "edps";
  let q = File.in_channel "data/ap/actor/zap.0.json" Ezjsonm.from_channel
          |> As2_vocab.Activitypub.Decode.person
          |> Result.get_ok in
  q.preferred_username |> Option.get |> Assrt.equals_string __LOC__ "mike";
  let q = File.in_channel "data/ap/actor/bridgy.0.json" Ezjsonm.from_channel
          |> As2_vocab.Activitypub.Decode.person
          |> Result.get_ok in
  q.preferred_username |> Option.get |> Assrt.equals_string __LOC__ "tantek.com";
  assert true

let test_encode () =
  let minify = false in
  let base = Uri.of_string "http://example.com/foo/" in
  let module E = Decoders_ezjsonm.Encode in
  E.encode_string E.obj [("k", `String "v")]
  |> Assrt.equals_string __LOC__ {|{"k":"v"}|};
  Ezjsonm.value_to_string (E.obj [("k", `String "v")])
  |> Assrt.equals_string __LOC__ {|{"k":"v"}|};

  let e : Rfc4287.Entry.t = {
    id         = Uri.make ~path:"a/b/" ?fragment:(Some "c") ();
    in_reply_to = [];
    lang       = Rfc4287.Rfc4646 "de";
    author     = Uri.make ~userinfo:"bar" ();
    title      = "uhu";
    published  = (Rfc3339.T "2023-03-07T01:23:45Z") ;
    updated    = (Rfc3339.T "2023-03-07T01:23:46Z");
    links      = [];
    categories = [
      ((Label (Single "lbl")),(Term (Single "term")),Uri.make ~path:"t/" ());
    ];
    content    = "Das war aber einfach";
  } in
  let n = Ap.Note.of_rfc4287 e in
  let j = n |> As2_vocab.Encode.note ~base in
  j |> Ezjsonm.value_to_string ~minify
  |> Assrt.equals_string __LOC__ {|{
  "type": "Note",
  "id": "http://example.com/foo/a/b/#c",
  "actor": "http://example.com/foo/activitypub/actor.jsa",
  "to": [
    "https://www.w3.org/ns/activitystreams#Public"
  ],
  "cc": [
    "http://example.com/foo/activitypub/notify/index.jsa"
  ],
  "mediaType": "text/plain; charset=utf8",
  "content": "Das war aber einfach",
  "sensitive": false,
  "summary": "uhu",
  "published": "2023-03-07T01:23:45Z",
  "tags": [
    {
      "type": "Hashtag",
      "href": "http://example.com/foo/t/term/",
      "name": "#lbl"
    }
  ],
  "url": []
}|};

  let co : 'a As2_vocab.Types.collection_page = {
    id = Uri.of_string "http://example.com/foo/";
    current    = None;
    first      = None;
    is_ordered = true;
    items      = [Ap.Note.Create.make n];
    last       = None;
    next       = None;
    part_of    = None;
    prev       = None;
    total_items= None;
  } in
  let j = As2_vocab.Encode.collection_page ~base
      (As2_vocab.Encode.create ~base
         (As2_vocab.Encode.note ~base))
      co in
  j |> Ezjsonm.value_to_string ~minify
  |> Assrt.equals_string __LOC__ {|{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1",
    {
      "schema": "http://schema.org#",
      "PropertyValue": "schema:PropertyValue",
      "value": "schema:value",
      "@language": "und"
    }
  ],
  "type": "OrderedCollectionPage",
  "id": "http://example.com/foo/",
  "orderedItems": [
    {
      "type": "Create",
      "id": "http://example.com/foo/a/b/#c/Create",
      "actor": "http://example.com/foo/activitypub/actor.jsa",
      "published": "2023-03-07T01:23:45Z",
      "to": [
        "https://www.w3.org/ns/activitystreams#Public"
      ],
      "cc": [
        "http://example.com/foo/activitypub/notify/index.jsa"
      ],
      "directMessage": false,
      "object": {
        "type": "Note",
        "id": "http://example.com/foo/a/b/#c",
        "actor": "http://example.com/foo/activitypub/actor.jsa",
        "to": [
          "https://www.w3.org/ns/activitystreams#Public"
        ],
        "cc": [
          "http://example.com/foo/activitypub/notify/index.jsa"
        ],
        "mediaType": "text/plain; charset=utf8",
        "content": "Das war aber einfach",
        "sensitive": false,
        "summary": "uhu",
        "published": "2023-03-07T01:23:45Z",
        "tags": [
          {
            "type": "Hashtag",
            "href": "http://example.com/foo/t/term/",
            "name": "#lbl"
          }
        ],
        "url": []
      }
    }
  ]
}|};

  assert true

(* https://www.w3.org/TR/activitystreams-core/#ex17-jsonld *)
let test_ex15_note () =
  let j = File.in_channel "data/ap/note/as2_core.ex15.json" Ezjsonm.value_from_channel in
  (match j with
   | `O [
       "@context", _ ;
       "summary", _ ;
       "type", `String "Create" ;
       "actor",  _ ;
       "object", `O [
         "type", `String "Note" ;
         _]
     ] -> assert true
   | _ -> assert false);
  let p = j
          |> As2_vocab.Decode.(create note)
          |> Result.is_error in
  assert p
  (*
  _p.obj.summary |> Option.get |> Assrt.equals_string "" "";
*)

(* https://github.com/mattjbray/ocaml-decoders *)
type role = Admin | User

type user =
  { lang : string
  ; txt  : role list
  }

let test_example() =
  Logr.info (fun m -> m "%s.%s" "As2_vocab" "example");
  let module My_encoders(E : Decoders.Encode.S) = struct
    open E

    let user : role encoder =
      function
      | Admin -> string "ADMIN"
      | User -> string "USER"

    let user : user encoder =
      fun u ->
      obj
        [ ("name", string u.lang)
        ; ("roles", list user u.txt)
        ]
  end in
  let module E = Decoders_ezjsonm.Encode in
  let module My_ezjson_encoders = My_encoders(Decoders_ezjsonm.Encode) in
  let open My_ezjson_encoders in
  let users =
    [ {lang = "Alice"; txt = [Admin; User]}
    ; {lang = "Bob"; txt = [User]}
    ] in
  E.encode_string E.obj [("users", E.list user users)]
  |> Assrt.equals_string __LOC__ {|{"users":[{"name":"Alice","roles":["ADMIN","USER"]},{"name":"Bob","roles":["USER"]}]}|}

type _i18n =
  { lang : string
  ; txt  : string
  }

let test_encode_content_map () =
  Logr.info (fun m -> m "%s.%s" "As2_vocab" "encode_content_map");
  let l = [("a","A");("b","B")] in
  let module E = Decoders_ezjsonm.Encode in
  let j = l |> List.map (fun (k,v) -> (k,E.string v)) in
  E.encode_string E.obj j
  |> Assrt.equals_string __LOC__ {|{"a":"A","b":"B"}|}

let test_decode_content_map () =
  Logr.info (fun m -> m "%s.%s" "As2_vocab" "decode_content_map");
  let s = Ezjsonm.value_from_string {|{"a":"A","b":"B"}|} in
  let module D = Decoders_ezjsonm.Decode in
  let l = D.key_value_pairs D.string s
          |> Result.get_ok in
  (match l with
   | [("a","A");("b","B")] -> ()
   | _ ->  "" |> Assrt.equals_string __LOC__ {|{"a":"A","b":"B"}|});
  let s = Ezjsonm.value_from_string {|{"contentMap":{"a":"A","b":"B","b":"C"}}|} in
  let l = D.field "contentMap" (D.key_value_pairs D.string) s
          |> Result.get_ok in
  match l with
  | [("a","A");("b","B");("b","C")] -> ()
  | _ ->  "" |> Assrt.equals_string __LOC__ {|{"a":"A","b":"B"}|}

let test_decode_natur () =
  (* https://codeberg.org/seppo/seppo/issues/5 *)
  Logr.info (fun m -> m "%s.%s" "As2_vocab" "decode_natur");
  let j = File.in_channel "data/ap/actor/natur.0.json" Ezjsonm.from_channel in
  let e = j |> As2_vocab.Decode.person |> Result.get_error in
  let s = e |> Decoders_ezjsonm.Decode.string_of_error in
  s |> Assrt.equals_string __LOC__ {|Expected an object with an attribute "publicKey", but got
{"id":"https://dev.rdf-pub.org/d613b246-8984-4654-903d-8d44143aca40","type":"Person","inboxSparql":"https://dev.rdf-pub.org/d613b246-8984-4654-903d-8d44143aca40/inbox/sparql","rdfpub:oauth2Issuer":"https://login.m4h.network/auth/realms/LOA","rdfpub:oauth2IssuerPreferredUserName":"max@login.m4h.network","rdfpub:oauth2IssuerUserId":"1813bdc1-152c-4c27-92a6-6cdfe401ef3d@login.m4h.network","outboxSparql":"https://dev.rdf-pub.org/d613b246-8984-4654-903d-8d44143aca40/outbox/sparql","identifier":"52ff7eb2-0b7c-4388-9894-b40a27714c1b","version":{"type":"xsd:integer","@value":"1"},"owl:sameAs":{"id":"https://dev.rdf-pub.org/05a75688-c517-4ae1-842c-5da3d8460627"},"inbox":"https://dev.rdf-pub.org/d613b246-8984-4654-903d-8d44143aca40/inbox","endpoints":{"oauthAuthorizationEndpoint":"https://dev.rdf-pub.org/oauth/oauthAuthorizationEndpoint","oauthTokenEndpoint":"https://dev.rdf-pub.org/oauth/oauthTokenEndpoint"},"name":"max","outbox":"https://dev.rdf-pub.org/d613b246-8984-4654-903d-8d44143aca40/outbox","published":"2024-01-14T15:59:42.102+01:00","@context":["https://schema.org/docs/jsonldcontext.json","https://rdf-pub.org/schema/rdf-pub-context.json","https://www.w3.org/ns/activitystreams"]}|}

let test_decode_sharkey() =
  (* https://joinsharkey.org *)
  Logr.info (fun m -> m "%s.%s" "As2_vocab" "decode_sharkey");
  let j = File.in_channel "data/ap/actor/sharkey.0.json" Ezjsonm.from_channel in
  let p = j |> As2_vocab.Decode.person |> Result.get_ok in
  p.name
  |> Option.value ~default:"-"
  |> Assrt.equals_string __LOC__ {|-|};
  (File.in_channel "data/ap/actor/sharkey.1.json" Ezjsonm.from_channel
   |> As2_vocab.Decode.person
   |> Result.get_ok)
  .name
  |> Option.value ~default:"-"
  |> Assrt.equals_string __LOC__ {|wakest the shark possum|}

let () =
  Unix.chdir "../../../test/";
  test_person ();
  test_actor_3rd ();
  test_note_decode ();
  test_webfinger_sunshine ();
  test_profile_sunshine ();
  test_encode ();
  test_ex15_note ();
  test_example ();
  test_encode_content_map ();
  test_decode_content_map ();
  test_decode_natur ();
  test_decode_sharkey();
  assert true

