(*
 * Copyright (c) 2016 - present Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *)

open! Utils

module F = Format

module MockTrace = Trace.Make(struct
    module MockTraceElem = struct
      type kind = unit
      type t = CallSite.t
      let call_site t = t
      let kind _ = ()
      let make _ site = site
      let compare = CallSite.compare
      let equal = CallSite.equal
      let pp = CallSite.pp
      let pp_kind _ _ = assert false

      let to_callee t _ = t

      module Set = PrettyPrintable.MakePPSet(struct
          type nonrec t = t
          let compare = compare
          let pp_element = pp
        end)

    end

    module Source = struct
      include MockTraceElem

      let get site =
        if string_is_prefix "SOURCE" (Procname.to_string (CallSite.pname site))
        then [(0, site)]
        else []

      let is_footprint _ = assert false
      let make_footprint _ = assert false
      let get_footprint_access_path _ = assert false
    end

    module Sink = struct
      include MockTraceElem

      let get site =
        if string_is_prefix "SINK" (Procname.to_string (CallSite.pname site))
        then [Sink.make_sink_param site 0 ~report_reachable:false]
        else []
    end

    let should_report _ _ = false

    let get_reportable_exn _ _ _ = assert false
  end)

module MockTaintAnalysis = TaintAnalysis.Make(struct
    include MockTrace

    let of_summary_trace _ = assert false
    let to_summary_trace _ = assert false
    let handle_unknown _ _ = []
  end)

module TestInterpreter = AnalyzerTester.Make
    (ProcCfg.Normal)
    (Scheduler.ReversePostorder)
    (MockTaintAnalysis.TransferFunctions)

let tests =
  let open OUnit2 in
  let open AnalyzerTester.StructuredSil in
  (* less verbose form of pretty-printing to make writing tests easy *)
  let pp_sparse fmt astate =
    let pp_call_site fmt call_site =
      F.fprintf fmt "%a" Procname.pp (CallSite.pname call_site) in
    let pp_sources fmt sources =
      if MockTrace.Sources.is_empty sources
      then F.fprintf fmt "?"
      else
        MockTrace.Sources.iter
          (fun source -> pp_call_site fmt (MockTrace.Source.call_site source))
          sources in
    let pp_sinks fmt sinks =
      if MockTrace.Sinks.is_empty sinks
      then F.fprintf fmt "?"
      else
        MockTrace.Sinks.iter
          (fun sink ->
             pp_call_site fmt (MockTrace.Sink.call_site sink))
          sinks in
    (* just print source -> sink, no line nums or passthroughs *)
    let pp_trace fmt trace =
      F.fprintf
        fmt
        "(%a -> %a)"
        pp_sources (MockTrace.sources trace)
        pp_sinks (MockTrace.sinks trace) in
    let pp_item fmt (ap, trace) =
      F.fprintf fmt "%a => %a" AccessPath.pp ap pp_trace trace in
    (* flatten access tree into list of access paths with associated traces *)
    let trace_assocs =
      MockTaintAnalysis.TaintDomain.fold
        (fun acc ap t ->
           if not (MockTrace.is_empty t)
           then (ap, t) :: acc
           else acc)
        astate.MockTaintAnalysis.Domain.access_tree
        [] in
    PrettyPrintable.pp_collection ~pp_item fmt (IList.rev trace_assocs) in
  let assign_to_source ret_str =
    let procname = Procname.from_string_c_fun "SOURCE" in
    make_call ~procname (Some (ident_of_str ret_str, dummy_typ)) [] in
  let assign_to_non_source ret_str =
    let procname = Procname.from_string_c_fun "NON-SOURCE" in
    make_call ~procname (Some (ident_of_str ret_str, dummy_typ)) [] in
  let call_sink_with_exp exp =
    let procname = Procname.from_string_c_fun "SINK" in
    make_call ~procname None [(exp, dummy_typ)] in
  let call_sink actual_str =
    call_sink_with_exp (Exp.Var (ident_of_str actual_str)) in
  let assign_id_to_field root_str fld_str rhs_id_str =
    let rhs_exp = Exp.Var (ident_of_str rhs_id_str) in
    make_store ~rhs_typ:Typ.Tvoid (Exp.Var (ident_of_str root_str)) fld_str ~rhs_exp in
  let read_field_to_id lhs_id_str root_str fld_str =
    make_load_fld ~rhs_typ:Typ.Tvoid lhs_id_str fld_str (Exp.Var (ident_of_str root_str)) in
  let assert_empty = invariant "{  }" in
  let test_list = [
    "source recorded",
    [
      assign_to_source "ret_id";
      invariant "{ ret_id$0 => (SOURCE -> ?) }";
    ];
    "non-source not recorded",
    [
      assign_to_non_source "ret_id";
      assert_empty;
    ];
    "source flows to var",
    [
      assign_to_source "ret_id";
      var_assign_id "var" "ret_id";
      invariant "{ ret_id$0 => (SOURCE -> ?), &var => (SOURCE -> ?) }";
    ];
    "source flows to field",
    [
      assign_to_source "ret_id";
      assign_id_to_field "base_id" "f" "ret_id";
      invariant "{ base_id$0.f => (SOURCE -> ?), ret_id$0 => (SOURCE -> ?) }";
    ];
    "source flows to field then var",
    [
      assign_to_source "ret_id";
      assign_id_to_field "base_id" "f" "ret_id";
      read_field_to_id "read_id" "base_id" "f";
      var_assign_id "var" "read_id";
      invariant
        "{ base_id$0.f => (SOURCE -> ?), ret_id$0 => (SOURCE -> ?), &var => (SOURCE -> ?) }";
    ];
    "source flows to var then cleared",
    [
      assign_to_source "ret_id";
      var_assign_id "var" "ret_id";
      invariant "{ ret_id$0 => (SOURCE -> ?), &var => (SOURCE -> ?) }";
      assign_to_non_source "non_source_id";
      var_assign_id "var" "non_source_id";
      invariant "{ ret_id$0 => (SOURCE -> ?) }";
    ];
    "source flows to field then cleared",
    [
      assign_to_source "ret_id";
      assign_id_to_field "base_id" "f" "ret_id";
      invariant "{ base_id$0.f => (SOURCE -> ?), ret_id$0 => (SOURCE -> ?) }";
      assign_to_non_source "non_source_id";
      assign_id_to_field "base_id" "f" "non_source_id";
      invariant "{ ret_id$0 => (SOURCE -> ?) }";
    ];
    "var id alias test",
    [
      assign_to_non_source "ret_id";
      var_assign_id "var1" "ret_id";
      assign_to_source "source_id";
      assign_id_to_field "ret_id" "f" "source_id";
      invariant "{ source_id$0 => (SOURCE -> ?), &var1.f => (SOURCE -> ?) }";
      read_field_to_id "read_id" "ret_id" "f";
      invariant "{ source_id$0 => (SOURCE -> ?), &var1.f => (SOURCE -> ?) }";
      var_assign_id "var2" "read_id";
      invariant
        "{ source_id$0 => (SOURCE -> ?), &var1.f => (SOURCE -> ?), &var2 => (SOURCE -> ?) }";
    ];
    "field id alias test1",
    [
      assign_to_non_source "ret_id";
      assign_to_source "source_id";
      assign_id_to_field "ret_id" "g" "source_id";
      assign_to_non_source "var_id";
      var_assign_id "var" "var_id";
      assign_id_to_field "var_id" "f" "ret_id";
      invariant
        "{ ret_id$0.g => (SOURCE -> ?), source_id$0 => (SOURCE -> ?), &var.f.g => (SOURCE -> ?) }";
    ];
    "field id alias test2",
    [
      assign_to_non_source "ret_id";
      read_field_to_id "g_id" "ret_id" "g";
      var_assign_id "var1" "g_id";
      assign_to_source "source_id";
      invariant "{ source_id$0 => (SOURCE -> ?) }";
      assign_id_to_field "g_id" "f" "source_id";
      invariant "{ source_id$0 => (SOURCE -> ?), &var1.g.f => (SOURCE -> ?) }";
      id_assign_var "var_id" "var1";
      read_field_to_id "var_g_id" "var_id" "g";
      read_field_to_id "var_g_f_id" "var_g_id" "f";
      var_assign_id "var2" "var_g_f_id";
      invariant
        "{ source_id$0 => (SOURCE -> ?), &var1.g.f => (SOURCE -> ?), &var2 => (SOURCE -> ?) }";
    ];
    "sink without source not tracked",
    [
      assign_to_non_source "ret_id";
      call_sink "ret_id";
      assert_empty;
    ];
    "source -> sink direct",
    [
      assign_to_source "ret_id";
      call_sink "ret_id";
      invariant "{ ret_id$0 => (SOURCE -> SINK) }";
    ];
    "source -> sink via var",
    [
      assign_to_source "ret_id";
      var_assign_id "actual" "ret_id";
      call_sink_with_exp (var_of_str "actual");
      invariant "{ ret_id$0 => (SOURCE -> ?), &actual => (SOURCE -> SINK) }";
    ];
    "source -> sink via var then ident",
    [
      assign_to_source "ret_id";
      var_assign_id "x" "ret_id";
      id_assign_var "actual_id" "x";
      call_sink "actual_id";
      invariant "{ ret_id$0 => (SOURCE -> ?), &x => (SOURCE -> SINK) }";
    ];
    "source -> sink via field",
    [
      assign_to_source "ret_id";
      assign_id_to_field "base_id" "f" "ret_id";
      read_field_to_id "actual_id" "base_id" "f";
      call_sink "actual_id";
      invariant "{ base_id$0.f => (SOURCE -> SINK), ret_id$0 => (SOURCE -> ?) }";
    ];
    "source -> sink via field read from var",
    [
      assign_to_source "ret_id";
      assign_id_to_field "base_id" "f" "ret_id";
      var_assign_id "var" "base_id";
      id_assign_var "var_id" "var";
      read_field_to_id "read_id" "var_id" "f";
      call_sink "read_id";
      invariant
        "{ base_id$0.f => (SOURCE -> ?), ret_id$0 => (SOURCE -> ?), &var.f => (SOURCE -> SINK) }";
    ];
    "source -> sink via cast",
    [
      assign_to_source "ret_id";
      cast_id_to_id "cast_id" Typ.Tvoid "ret_id";
      call_sink "cast_id";
      invariant "{ ret_id$0 => (SOURCE -> SINK) }";
    ];

  ] |> TestInterpreter.create_tests ~pp_opt:pp_sparse AccessPath.BaseMap.empty in
  "taint_test_suite">:::test_list
