#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# pytype: skip-file

import contextlib
import logging
import os
import tempfile
import unittest
import zipfile

import requests_mock

from apache_beam.options import pipeline_options
from apache_beam.portability.api import beam_job_api_pb2
from apache_beam.portability.api import beam_runner_api_pb2
from apache_beam.runners.portability import flink_runner
from apache_beam.runners.portability import flink_uber_jar_job_server
from apache_beam.runners.portability.local_job_service_test import TestJobServicePlan


@contextlib.contextmanager
def temp_name(*args, **kwargs):
  with tempfile.NamedTemporaryFile(*args, **kwargs) as t:
    name = t.name
  try:
    yield name
  finally:
    if os.path.exists(name):
      os.unlink(name)


class FlinkUberJarJobServerTest(unittest.TestCase):
  @requests_mock.mock()
  def test_flink_version(self, http_mock):
    http_mock.get('http://flink/v1/config', json={'flink-version': '3.1.4.1'})
    job_server = flink_uber_jar_job_server.FlinkUberJarJobServer(
        'http://flink', pipeline_options.FlinkRunnerOptions())
    self.assertEqual(job_server.flink_version(), "3.1")

  @requests_mock.mock()
  def test_get_job_metrics(self, http_mock):
    response = {
        "user-task-accumulators": [{
            "name": "__metricscontainers",
            "type": "MetricsAccumulator",
            "value": "{\"metrics\": {\"attempted\": [{\"urn\": "
            "\"metric_urn\", \"type\": \"beam:metrics:sum_int64:v1\", "
            "\"payload\": \"AA==\", \"labels\": "
            "{\"PTRANSFORM\": \"ptransform_id\"}}]}}"
        }]
    }
    http_mock.get(
        'http://flink/v1/jobs/flink_job_id/accumulators', json=response)
    options = pipeline_options.FlinkRunnerOptions()
    job_server = flink_uber_jar_job_server.FlinkUberJarJobServer(
        'http://flink', options)
    job = flink_uber_jar_job_server.FlinkBeamJob(
        'http://flink', None, 'job_id', 'job_name', None, options)
    job._flink_job_id = 'flink_job_id'
    job_server._jobs['job_id'] = job
    request = beam_job_api_pb2.GetJobMetricsRequest(job_id='job_id')
    expected = beam_job_api_pb2.GetJobMetricsResponse(
        metrics=beam_job_api_pb2.MetricResults(
            attempted=[{
                "urn": "metric_urn",
                "type": "beam:metrics:sum_int64:v1",
                "payload": b'\000',
                "labels": {
                    "PTRANSFORM": "ptransform_id"
                }
            }]))

    actual = job_server.GetJobMetrics(request)
    self.assertEqual(actual, expected)

  @requests_mock.mock()
  def test_end_to_end(self, http_mock):
    with temp_name(suffix='fake.jar') as fake_jar:
      # Create the jar file with some trivial contents.
      with zipfile.ZipFile(fake_jar, 'w') as zip:
        with zip.open('FakeClass.class', 'w') as fout:
          fout.write(b'[original_contents]')

      options = pipeline_options.FlinkRunnerOptions()
      options.flink_job_server_jar = fake_jar
      job_server = flink_uber_jar_job_server.FlinkUberJarJobServer(
          'http://flink', options)

      plan = TestJobServicePlan(job_server)

      # Prepare the job.
      prepare_response = plan.prepare(beam_runner_api_pb2.Pipeline())
      plan.stage(
          beam_runner_api_pb2.Pipeline(),
          prepare_response.artifact_staging_endpoint.url,
          prepare_response.staging_session_token)

      # Now actually run the job.
      http_mock.post(
          'http://flink/v1/jars/upload',
          json={'filename': '/path/to/jar/nonce'})
      http_mock.post(
          'http://flink/v1/jars/nonce/run', json={'jobid': 'some_job_id'})

      _, message_stream, state_stream = plan.run(
          prepare_response.preparation_id)

      # Check the status until the job is "done" and get all error messages.
      http_mock.get(
          'http://flink/v1/jobs/some_job_id/execution-result',
          [{
              'json': {
                  'status': {
                      'id': 'IN_PROGRESS'
                  }
              }
          }, {
              'json': {
                  'status': {
                      'id': 'IN_PROGRESS'
                  }
              }
          }, {
              'json': {
                  'status': {
                      'id': 'COMPLETED'
                  }
              }
          }])
      http_mock.get(
          'http://flink/v1/jobs/some_job_id', json={'state': 'FINISHED'})
      http_mock.delete('http://flink/v1/jars/nonce')

      self.assertEqual([s.state for s in state_stream],
                       [
                           beam_job_api_pb2.JobState.STOPPED,
                           beam_job_api_pb2.JobState.RUNNING,
                           beam_job_api_pb2.JobState.RUNNING,
                           beam_job_api_pb2.JobState.DONE
                       ])

      http_mock.get(
          'http://flink/v1/jobs/some_job_id/exceptions',
          json={'all-exceptions': [{
              'exception': 'exc_text', 'timestamp': 0
          }]})

      def get_item(x):
        if x.HasField('message_response'):
          return x.message_response
        else:
          return x.state_response.state

      self.assertEqual([get_item(m) for m in message_stream],
                       [
                           beam_job_api_pb2.JobState.STOPPED,
                           beam_job_api_pb2.JobState.RUNNING,
                           beam_job_api_pb2.JobMessage(
                               message_id='message0',
                               time='0',
                               importance=beam_job_api_pb2.JobMessage.
                               MessageImportance.JOB_MESSAGE_ERROR,
                               message_text='exc_text'),
                           beam_job_api_pb2.JobState.DONE,
                       ])

  def test_retain_unknown_options(self):
    original_options = pipeline_options.PipelineOptions(
        ['--unknown_option_foo=some_value'])
    flink_options = original_options.view_as(
        pipeline_options.FlinkRunnerOptions)
    flink_options.flink_submit_uber_jar = True
    flink_options.flink_master = 'http://host:port'
    runner = flink_runner.FlinkRunner()

    job_service_handle = runner.create_job_service(original_options)
    options_proto = job_service_handle.get_pipeline_options()

    self.assertEqual(
        options_proto['beam:option:unknown_option_foo:v1'], 'some_value')

  @requests_mock.mock()
  def test_bad_url_flink_version(self, http_mock):
    http_mock.get('http://flink/v1/config', json={'flink-version': '1.2.3.4'})
    options = pipeline_options.FlinkRunnerOptions()
    options.flink_job_server_jar = "bad url"
    job_server = flink_uber_jar_job_server.FlinkUberJarJobServer(
        'http://flink', options)
    with self.assertRaises(ValueError) as context:
      job_server.executable_jar()
    self.assertEqual(
        'Unable to parse jar URL "bad url". If using a full URL, make sure '
        'the scheme is specified. If using a local file path, make sure '
        'the file exists; you may have to first build the job server '
        'using `./gradlew runners:flink:1.2:job-server:shadowJar`.',
        str(context.exception))

  def test_bad_url_placeholder_version(self):
    options = pipeline_options.FlinkRunnerOptions()
    options.flink_job_server_jar = "bad url"
    job_server = flink_uber_jar_job_server.FlinkUberJarJobServer(
        'http://example.com/bad', options)
    with self.assertRaises(ValueError) as context:
      job_server.executable_jar()
    self.assertEqual(
        'Unable to parse jar URL "bad url". If using a full URL, make sure '
        'the scheme is specified. If using a local file path, make sure '
        'the file exists; you may have to first build the job server '
        'using `./gradlew runners:flink:$FLINK_VERSION:job-server:shadowJar`.',
        str(context.exception))


if __name__ == '__main__':
  logging.getLogger().setLevel(logging.INFO)
  unittest.main()
