CREATE SCHEMA _null_count_test;

-- See bottom as well!
SET SEARCH_PATH = _null_count_test, tap;

CREATE FUNCTION ncs() RETURNS name IMMUTABLE LANGUAGE sql AS $$
SELECT nspname FROM pg_namespace n JOIN pg_extension x ON n.oid = x.extnamespace WHERE extname = 'count_nulls'
$$;
/*
 * NOTE! Do not use create or replace function in here. If you do that and
 * accidentally try to define the same function twice you'll never detect that
 * mistake!
 */

/*
CREATE FUNCTION test__
() RETURNS SETOF text LANGUAGE plpgsql AS $body$
DECLARE
BEGIN
END
$body$;
*/

/*
 * function definition
 */
CREATE FUNCTION test__definition
() RETURNS SETOF text LANGUAGE plpgsql AS $body$
DECLARE
  f_name name;
  t text;
  f_args text[];
BEGIN
  FOREACH f_name IN ARRAY '{null_count,not_null_count}'::name[] LOOP
    FOREACH t IN ARRAY '{anyarray,json,jsonb}'::text[] LOOP
      f_args := array[t];

      -- If the installed schema isn't in our search path
      IF NOT current_schemas(true)@>array[ncs()] THEN
        RETURN NEXT hasnt_function(
          f_name, f_args
          , format('ensure %s(%s) is not in search_path', f_name, f_args)
        );
      END IF;

      RETURN NEXT function_returns(
        ncs(), f_name, f_args
        , 'int'::regtype::text -- Sanitize type name
      );

      -- TODO: isnt_definer
      RETURN NEXT isnt_strict(
        ncs(), f_name, f_args
      );

      RETURN NEXT volatility_is(
        ncs(), f_name, f_args
        , 'immutable'
      );
    END LOOP;
  END LOOP;

  FOREACH f_name IN ARRAY '{null_count_trigger,not_null_count_trigger}'::name[] LOOP
    f_args := '{}';
    RETURN NEXT function_returns(
      ncs(), f_name, f_args
      , 'trigger'
    );

    -- TODO: isnt_definer
    RETURN NEXT isnt_strict(
      ncs(), f_name, f_args
    );

    RETURN NEXT volatility_is(
      ncs(), f_name, f_args
      , 'immutable'
    );
  END LOOP;
END
$body$;

/*
 * Operation
 */

CREATE FUNCTION pg_temp.test_trigger_raw(
  trigger_name name
  , ba text
  , exec text
  , errmsg text
  , errdesc text

) RETURNS SETOF text LANGUAGE plpgsql AS $body$
DECLARE
  c_command CONSTANT text :=
    format( $$CREATE TRIGGER %s %s INSERT ON test_data FOR EACH ROW EXECUTE PROCEDURE %s$$
      , trigger_name
      , ba
      , exec
    )
  ;
BEGIN
  RETURN NEXT lives_ok( c_command, c_command );
  RETURN NEXT throws_ok(
    $$INSERT INTO test_data VALUES (1,1,NULL)$$
    , 'P0001'
    , errmsg
    , errdesc
  );

  RETURN NEXT lives_ok(
    format( 'DROP TRIGGER %s ON test_data', trigger_name )
    , format( 'DROP TRIGGER %s', trigger_name )
  );
END
$body$;

CREATE FUNCTION pg_temp.test_trigger(
  ba text
  , nn text
  , err text
) RETURNS SETOF text LANGUAGE plpgsql AS $body$
DECLARE
  c_trigger_name CONSTANT text := quote_ident(format('%s_%s_%s', nn, ba, err));
BEGIN
  RETURN QUERY
    SELECT pg_temp.test_trigger_raw(
      c_trigger_name
      , ba
      , exec := format(
          $$%I.%s_count_trigger(1, %L)$$
          , ncs()
          , nn
          , err
        )
      , errmsg := coalesce( err, format( 'test_data must contain 1 %s fields', upper( replace( nn, '_', ' ' ) ) ) )
      , errdesc := 'Test ' || c_trigger_name
    )
  ;
END
$body$;

CREATE FUNCTION test__functionality
() RETURNS SETOF text LANGUAGE plpgsql AS $body$
DECLARE
BEGIN
  CREATE TEMP TABLE test_data AS
    SELECT * FROM (
    VALUES
      -- Can't make first argument bigint without variant
        ( 1::int, 2::int, 3::int, 0 )
      , ( 1,    2,    NULL, 1 )
      , ( 1,    NULL, 3,    1 )
      , ( 1,    NULL, NULL, 2 )
      , ( NULL, 2,    3,    1 )
      , ( NULL, 2,    NULL, 2 )
      , ( NULL, NULL, 3,    2 )
      , ( NULL, NULL, NULL, 3 )
    ) AS a( a, b, c, null_count )
  ;

  RETURN NEXT bag_eq(
    format($$SELECT a, b, c, %1$I.null_count( a, b, c ), %1$I.not_null_count( a, b, c ) FROM test_data$$, ncs())
    , $$SELECT *, 3-null_count AS not_null_count FROM test_data$$
    , format('Test %I.null_count(a, b, c)', ncs())
  );

  -- Test JSON versions
  -- These could be combined...
  RETURN NEXT bag_eq(
    format($$SELECT a, b, c, %1$I.null_count( row_to_json( row(a, b, c) ) ), %1$I.not_null_count( row_to_json( row(a, b, c) ) ) FROM test_data$$, ncs())
    , $$SELECT *, 3-null_count AS not_null_count FROM test_data$$
    , format('Test %I.null_count(json)', ncs())
  );
  RETURN NEXT bag_eq(
    format($$SELECT a, b, c, %1$I.null_count( row_to_json( row(a, b, c) )::jsonb ), %1$I.not_null_count( row_to_json( row(a, b, c) )::jsonb ) FROM test_data$$, ncs())
    , $$SELECT *, 3-null_count AS not_null_count FROM test_data$$
    , format('Test %I.null_count(jsonb)', ncs())
  );

  -- Doesn't work for array types
  /*
  RETURN NEXT bag_eq(
    $$SELECT a, b, c, null_count( array[a], array[b], array[c] ) FROM test_data$$
    , $$SELECT * FROM test_data$$
    , 'Test null_count(array[a], array[b], array[c])'
  );
  */

  RETURN QUERY
    SELECT pg_temp.test_trigger_raw(
          '"test trigger"'
          , 'BEFORE'
          , trig
          , CASE
              WHEN args = '' AND not_ = 'not_'  THEN $$test trigger usage: number of NOT NULL columns[, error message]$$
              WHEN args = '' AND not_ = ''      THEN $$test trigger usage: number of NULL columns[, error message]$$
              ELSE 'test trigger: first argument must not be null'
            END
          , 'Test ' || trig
        )
      FROM (
        SELECT *, format( '%I.%snull_count_trigger( %s )', ncs(), not_, args ) AS trig
          FROM
            unnest( array['not_', ''] ) not_
            , unnest( array['NULL', ''] ) args
        ) a
  ;

  RETURN QUERY
    SELECT pg_temp.test_trigger( ba, nn, err )
      FROM
        unnest( '{"null",not_null}'::text[] ) AS nn(nn)
        , unnest( '{BEFORE,AFTER}'::text[] ) AS ba(ba)
        , unnest( '{"error_message",NULL}'::text[] ) AS err(err)
  ;

END
$body$;

SET SEARCH_PATH = _null_count_test, tap, :schema;

-- vi: expandtab sw=2 ts=2
