open OUnit2
open StdLabels
module A = ImportAnalyser.Dependency
module Cont = ImportContainers
module Syntax = ImportConf.Syntax
module Expression = ImportExpression.T
module Table = ImportDataTypes.Table
open ConfLoader

let test_order =
  "Order" >:: fun _ ->
  let order = A.get_process_order conf |> List.map ~f:A.table in
  let expected_order =
    [ external_table_last; external_table_other; external_table_source ]
  in
  assert_equal ~cmp:(cmp_list cmp_source) ~printer:show_sources expected_order
    order

let test_columns =
  "Columns" >:: fun _ ->
  let expected_colums = Cont.IntSet.of_list [ 1; 2 ] in
  let columns =
    A.get_process_order conf
    |> List.find ~f:(fun v -> A.table v = external_table_source)
    |> A.columns
  in

  assert_equal
    ~cmp:(fun a b -> 0 = Cont.IntSet.compare a b)
    ~printer:Cont.show_intSet expected_colums columns

let test_keys =
  "Keys" >:: fun _ ->
  (* We should have one key in the table other, because it is referenced as a
     source in last file *)
  let expected_keys =
    A.
      [
        {
          name = "other";
          expression = Expression.Path 3;
          columns = lazy (Cont.IntSet.singleton 3);
        };
      ]
  in

  let keys =
    A.get_process_order conf
    |> List.find ~f:(fun v -> A.table v = external_table_other)
    |> A.keys
  in

  assert_equal ~cmp:(cmp_list key_cmp) ~printer:keys_printer expected_keys keys

let test_keys_missing =
  "Keys missing" >:: fun _ ->
  (* We have no key in last_file because the key is declared as missing *)
  let expected_keys =
    A.
      [
        {
          name = "last_file";
          expression = Expression.Path 3;
          columns = lazy (Cont.IntSet.singleton 3);
        };
      ]
  in

  let keys =
    A.get_process_order conf
    |> List.find ~f:(fun v -> A.table v = external_table_last)
    |> A.keys
  in

  assert_equal ~cmp:(cmp_list key_cmp) ~printer:keys_printer expected_keys keys

let test_unknow_source =
  "Unknown source" >:: fun _ ->
  let conf = { conf with externals = [] } in
  assert_raises (ImportErrors.Unknown_source "last_file") (fun () ->
      A.get_process_order conf)

let test_unordered =
  "Unorderd references" >:: fun _ ->
  (* Externals not described in the right order shall not raise any
     error. *)
  let conf =
    load
      {|version = 1
[source]
  file = "source.xlsx"
  name = "source"

[externals.last_file]
  intern_key = ":other.A"
  file = "last.xlsx"
  extern_key = ":C"
  allow_missing = true 

[externals.other]
  intern_key = ":A"
  file = "other.xlsx"
  extern_key = ":C"
  allow_missing = false

[sheet]
  columns = []|}
  in
  assert_raises (ImportErrors.Unknown_source "other") (fun () ->
      A.get_process_order conf)

let test_circular =
  "Unlinked reference" >:: fun _ ->
  (* A reference to itself should be understood *)
  let conf =
    load
      {|version = 1
[source]
  file = "source.xlsx"
  name = "source"

[externals.circular]
  intern_key = ":circular.A"
  file = "last.xlsx"
  extern_key = ":A"
  allow_missing = true 

[sheet]
  columns = []|}
  in

  let elements = A.get_process_order conf in
  assert_equal ~printer:string_of_int 1 (List.length elements)

let test_unlinked =
  "Circular reference" >:: fun _ ->
  (* An element linked to anything (except itself) should be ignored *)
  let conf =
    Syntax.
      {
        version = 1;
        source = external_table_source;
        externals =
          [
            {
              intern_key = Path { alias = Some "circular2"; column = 1 };
              target = { file = "other.xlsx"; tab = 1; name = "circular" };
              extern_key = Path 3;
              allow_missing = true;
              match_rule = None;
            };
            {
              intern_key = Path { alias = Some "circular"; column = 1 };
              target = { file = "other2.xlsx"; tab = 1; name = "circular2" };
              extern_key = Path 3;
              allow_missing = true;
              match_rule = None;
            };
          ];
        columns = [];
        filters = [];
        sort = [];
        uniq = [];
      }
  in
  assert_raises (ImportErrors.Unknown_source "circular2") (fun () ->
      A.get_process_order conf |> List.map ~f:A.table)

let conf_with_unlinked =
  Syntax.
    {
      version = 1;
      source = external_table_source;
      externals =
        [
          {
            intern_key = Path { alias = None; column = 1 };
            target = { file = "other.xlsx"; tab = 1; name = "other" };
            extern_key = Path 3;
            allow_missing = false;
            match_rule = None;
          };
        ];
      columns =
        [
          Concat [ Path { alias = None; column = 1 }; Literal "_"; Empty ];
          Path { alias = None; column = 2 };
        ];
      filters = [];
      sort = [];
      uniq = [];
    }

(** A table referenced only in a filter list shall be loaded correctly *)
let test_order_filter =
  "Order filter" >:: fun _ ->
  let order =
    {
      conf_with_unlinked with
      filters = [ Path { alias = Some "other"; column = 5 } ];
    }
    |> A.get_process_order |> List.map ~f:A.table
  in
  let expected_order = [ external_table_other; external_table_source ] in
  assert_equal ~printer:show_sources expected_order order

(** A table referenced only in the order list shall be loaded correctly *)
let test_order_sort =
  "Order sort" >:: fun _ ->
  let order =
    {
      conf_with_unlinked with
      sort = [ Path { alias = Some "other"; column = 5 } ];
    }
    |> A.get_process_order |> List.map ~f:A.table
  in
  let expected_order = [ external_table_other; external_table_source ] in
  assert_equal ~printer:show_sources expected_order order

(** A table referenced only in the uniq list shall be loaded correctly *)
let test_order_uniq =
  "Order uniq" >:: fun _ ->
  let order =
    {
      conf_with_unlinked with
      uniq = [ Path { alias = Some "other"; column = 5 } ];
    }
    |> A.get_process_order |> List.map ~f:A.table
  in
  let expected_order = [ external_table_other; external_table_source ] in
  assert_equal ~printer:show_sources expected_order order

let tests =
  "analyser_dependency"
  >::: [
         test_order;
         test_columns;
         test_keys;
         test_keys_missing;
         test_unknow_source;
         test_unordered;
         test_circular;
         test_unlinked;
         test_order_filter;
         test_order_sort;
         test_order_uniq;
       ]