import {
  ACTION,
  reducer,
} from 'app/pages/querybuilder/context/query-builder-provider';

const state = reducer();

test('default state', () => {
  const fixed = { ...state, params: hideTypes(state.params) };
  expect(fixed).toEqual({
    http: {},
    params: {
      value: [{ id: '0', value: '', type: 'yql' }],
    },
    query: {
      input: '{\n    "yql": ""\n}',
    },
    request: {
      body: '{\n    "yql": ""\n}',
      fullUrl: 'http://localhost:8080/search/',
      method: 'POST',
      url: 'http://localhost:8080/search/',
    },
  });
});

test('manipulates inputs', () => {
  function assert(state, queryJson, querySearchParams, params) {
    expect(hideTypes(state.params).value).toEqual(params);
    expect({ ...state.query, input: JSON.parse(state.query.input) }).toEqual(
      queryJson
    );

    const spState = reducer(state, { action: ACTION.SET_METHOD, data: 'GET' });
    expect(hideTypes(spState.params).value).toEqual(params);
    expect(spState.query).toEqual(querySearchParams);
  }

  const s1 = reduce(state, [[ACTION.INPUT_ADD, { type: 'hits' }]]);
  assert(s1, { input: { yql: '', hits: null } }, { input: 'yql=&hits=' }, [
    { id: '0', value: '', type: 'yql' },
    { id: '1', value: '', type: 'hits' },
  ]);

  const s2 = reduce(s1, [
    [ACTION.INPUT_UPDATE, { id: '1', value: '12' }],
    [ACTION.INPUT_ADD, { type: 'ranking' }],
    [ACTION.INPUT_UPDATE, { id: '1', type: 'offset' }],
    [ACTION.INPUT_REMOVE, '0'],
    [ACTION.INPUT_ADD, { id: '2', type: 'location' }],
    [ACTION.INPUT_ADD, { id: '2', type: 'matchPhase' }],
    [ACTION.INPUT_UPDATE, { id: '2.0', value: 'us' }],
    [ACTION.INPUT_ADD, { id: '2', type: 'features' }],
    [ACTION.INPUT_ADD, { id: '2.2', type: '' }],
    [ACTION.INPUT_UPDATE, { id: '2.2.0', type: 'abc' }],
    [ACTION.INPUT_UPDATE, { id: '2.2.0', value: '123' }],
  ]);
  assert(
    s2,
    {
      input: {
        offset: 12,
        ranking: { location: 'us', matchPhase: {}, features: { abc: '123' } },
      },
    },
    { input: 'offset=12&ranking.location=us&ranking.features.abc=123' },
    [
      { id: '1', value: '12', type: 'offset' },
      {
        id: '2',
        type: 'ranking',
        value: [
          { id: '2.0', value: 'us', type: 'location' },
          { id: '2.1', type: 'matchPhase', value: [] },
          {
            id: '2.2',
            type: 'features',
            value: [{ id: '2.2.0', type: 'abc', value: '123' }],
          },
        ],
      },
    ]
  );

  assert(
    reduce(s2, [[ACTION.INPUT_UPDATE, { id: '2', type: 'noCache' }]]),
    { input: { offset: 12, noCache: false } },
    { input: 'offset=12&noCache=' },
    [
      { id: '1', value: '12', type: 'offset' },
      { id: '2', value: '', type: 'noCache' },
    ]
  );

  assert(
    reduce(s2, [[ACTION.INPUT_REMOVE, '2']]),
    { input: { offset: 12 } },
    { input: 'offset=12' },
    [{ id: '1', value: '12', type: 'offset' }]
  );
});

test('set query', () => {
  const query = (method, input) =>
    reduce(state, [
      [ACTION.INPUT_REMOVE, '0'],
      [ACTION.SET_METHOD, method],
      [ACTION.SET_QUERY, input],
    ]);
  function assert(inputJson, inputSearchParams, params) {
    const s2 = query('POST', inputJson);
    expect(hideTypes(s2.params).value).toEqual(params);
    expect(s2.query.input).toEqual(inputJson);
    expect(s2.query.error).toBeUndefined();

    if (inputSearchParams == null) return;
    const s3 = query('GET', inputSearchParams);
    expect(hideTypes(s3.params).value).toEqual(params);
    expect(s3.query.input).toEqual(inputSearchParams);
    expect(s3.query.error).toBeUndefined();
  }

  function error(method, input, error) {
    const s = query(method, input);
    expect(s.params.value).toEqual([]);
    expect(s.query.input).toEqual(input);
    expect(s.query.error).toEqual(error);
  }

  assert('{"yql":"abc"}', '?yql=abc', [{ id: '0', value: 'abc', type: 'yql' }]);

  assert(
    '{"hits":12,"ranking":{"location":"us","matchPhase":{"attribute":"[\\"a b\\"]"}},"noCache":true,"offset":""}',
    'hits=12&ranking.location=us&noCache=true&ranking.matchPhase.attribute=%5B%22a+b%22%5D&offset',
    [
      { id: '0', value: '12', type: 'hits' },
      {
        id: '1',
        type: 'ranking',
        value: [
          { id: '1.0', value: 'us', type: 'location' },
          {
            id: '1.1',
            type: 'matchPhase',
            value: [{ id: '1.1.0', value: '["a b"]', type: 'attribute' }],
          },
        ],
      },
      { id: '2', value: 'true', type: 'noCache' },
      { id: '3', value: '', type: 'offset' },
    ]
  );

  assert('{"ranking":{"matchPhase":{}}}', null, [
    {
      id: '0',
      type: 'ranking',
      value: [{ id: '0.0', type: 'matchPhase', value: [] }],
    },
  ]);

  assert(
    '{"ranking":{"features":{"abc":"123","def":"456"}}}',
    'ranking.features.abc=123&ranking.features.def=456',
    [
      {
        id: '0',
        type: 'ranking',
        value: [
          {
            id: '0.0',
            type: 'features',
            value: [
              { id: '0.0.0', type: 'abc', value: '123' },
              { id: '0.0.1', type: 'def', value: '456' },
            ],
          },
        ],
      },
    ]
  );

  let msg = "Unknown property 'asd' on root level";
  error('POST', '{"asd":123}', msg);
  error('GET', 'asd=123', msg);

  msg = "Unknown property 'asd' under 'matchPhase'";
  error('POST', '{"ranking":{"matchPhase":{"asd":123}}}', msg);
  error('GET', 'ranking.matchPhase.asd=123', msg);

  error('POST', '{"yql":"test}', 'Unexpected end of JSON input');

  msg =
    "Property 'ranking' cannot have a value, supported children: features,freshness,listFeatures,location,matchPhase,matching,profile,properties,queryCache,rerankCount,softtimeout,sorting";
  error('POST', '{"ranking":123}', msg);
  error('GET', 'ranking=123', msg);

  error('POST', '{"yql":{}}', "Expected property 'yql' to be String");
});

function hideTypes({ type, value, ...copy }) {
  if (type.name) copy.type = type.name;
  copy.value = type.children ? value.map(hideTypes) : value;
  return copy;
}

function reduce(state, operations) {
  return operations.reduce(
    (acc, [action, data]) => reducer(acc, { action, data }),
    state
  );
}
