# -*- coding: utf-8 -*-

import hashlib
import json
import uuid

from datetime import datetime, timedelta
from io import BytesIO

import pytest

from mock import patch
from playhouse.test_utils import assert_query_count

from app import docker_v2_signing_key, storage
from data import model
from data.database import (TagManifestLabelMap, TagManifestToManifest, Manifest, ManifestBlob,
                           ManifestLegacyImage, ManifestLabel,
                           TagManifest, TagManifestLabel, DerivedStorageForImage,
                           TorrentInfo, Tag, TagToRepositoryTag, ImageStorageLocation)
from data.cache.impl import InMemoryDataModelCache
from data.registry_model.registry_pre_oci_model import PreOCIModel
from data.registry_model.registry_oci_model import OCIModel
from data.registry_model.datatypes import RepositoryReference
from data.registry_model.blobuploader import upload_blob, BlobUploadSettings
from data.registry_model.modelsplitter import SplitModel
from data.model.blob import store_blob_record_and_temp_link
from image.docker.types import ManifestImageLayer
from image.docker.schema1 import (DockerSchema1ManifestBuilder, DOCKER_SCHEMA1_CONTENT_TYPES,
                                  DockerSchema1Manifest)
from image.docker.schema2.manifest import DockerSchema2ManifestBuilder
from image.docker.schema2.list import DockerSchema2ManifestListBuilder
from util.bytes import Bytes

from test.fixtures import *


@pytest.fixture(params=[PreOCIModel(), OCIModel(), OCIModel(oci_model_only=False),
                        SplitModel(0, {'devtable'}, {'buynlarge'}, False),
                        SplitModel(1.0, {'devtable'}, {'buynlarge'}, False),
                        SplitModel(1.0, {'devtable'}, {'buynlarge'}, True)])
def registry_model(request, initialized_db):
  return request.param

@pytest.fixture()
def pre_oci_model(initialized_db):
  return PreOCIModel()

@pytest.fixture()
def oci_model(initialized_db):
  return OCIModel()


@pytest.mark.parametrize('names, expected', [
  (['unknown'], None),
  (['latest'], {'latest'}),
  (['latest', 'prod'], {'latest', 'prod'}),
  (['latest', 'prod', 'another'], {'latest', 'prod'}),
  (['foo', 'prod'], {'prod'}),
])
def test_find_matching_tag(names, expected, registry_model):
  repo = model.repository.get_repository('devtable', 'simple')
  repository_ref = RepositoryReference.for_repo_obj(repo)
  found = registry_model.find_matching_tag(repository_ref, names)
  if expected is None:
    assert found is None
  else:
    assert found.name in expected
    assert found.repository.namespace_name == 'devtable'
    assert found.repository.name == 'simple'


@pytest.mark.parametrize('repo_namespace, repo_name, expected', [
  ('devtable', 'simple', {'latest', 'prod'}),
  ('buynlarge', 'orgrepo', {'latest', 'prod'}),
])
def test_get_most_recent_tag(repo_namespace, repo_name, expected, registry_model):
  repo = model.repository.get_repository(repo_namespace, repo_name)
  repository_ref = RepositoryReference.for_repo_obj(repo)
  found = registry_model.get_most_recent_tag(repository_ref)
  if expected is None:
    assert found is None
  else:
    assert found.name in expected


@pytest.mark.parametrize('repo_namespace, repo_name, expected', [
  ('devtable', 'simple', True),
  ('buynlarge', 'orgrepo', True),
  ('buynlarge', 'unknownrepo', False),
])
def test_lookup_repository(repo_namespace, repo_name, expected, registry_model):
  repo_ref = registry_model.lookup_repository(repo_namespace, repo_name)
  if expected:
    assert repo_ref
  else:
    assert repo_ref is None


@pytest.mark.parametrize('repo_namespace, repo_name', [
  ('devtable', 'simple'),
  ('buynlarge', 'orgrepo'),
])
def test_lookup_manifests(repo_namespace, repo_name, registry_model):
  repo = model.repository.get_repository(repo_namespace, repo_name)
  repository_ref = RepositoryReference.for_repo_obj(repo)
  found_tag = registry_model.find_matching_tag(repository_ref, ['latest'])
  found_manifest = registry_model.get_manifest_for_tag(found_tag)
  found = registry_model.lookup_manifest_by_digest(repository_ref, found_manifest.digest,
                                                   include_legacy_image=True)
  assert found._db_id == found_manifest._db_id
  assert found.digest == found_manifest.digest
  assert found.legacy_image
  assert found.legacy_image.parents

  schema1_parsed = registry_model.get_schema1_parsed_manifest(found, 'foo', 'bar', 'baz', storage)
  assert schema1_parsed is not None


def test_lookup_unknown_manifest(registry_model):
  repo = model.repository.get_repository('devtable', 'simple')
  repository_ref = RepositoryReference.for_repo_obj(repo)
  found = registry_model.lookup_manifest_by_digest(repository_ref, 'sha256:deadbeef')
  assert found is None


@pytest.mark.parametrize('repo_namespace, repo_name', [
  ('devtable', 'simple'),
  ('devtable', 'complex'),
  ('devtable', 'history'),
  ('buynlarge', 'orgrepo'),
])
def test_legacy_images(repo_namespace, repo_name, registry_model):
  repository_ref = registry_model.lookup_repository(repo_namespace, repo_name)
  legacy_images = registry_model.get_legacy_images(repository_ref)
  assert len(legacy_images)

  found_tags = set()
  for image in legacy_images:
    found_image = registry_model.get_legacy_image(repository_ref, image.docker_image_id,
                                                  include_parents=True)

    with assert_query_count(5 if found_image.parents else 4):
      found_image = registry_model.get_legacy_image(repository_ref, image.docker_image_id,
                                                    include_parents=True, include_blob=True)
      assert found_image.docker_image_id == image.docker_image_id
      assert found_image.parents == image.parents
      assert found_image.blob
      assert found_image.blob.placements

    # Check that the tags list can be retrieved.
    assert image.tags is not None
    found_tags.update({tag.name for tag in image.tags})

    # Check against the actual DB row.
    model_image = model.image.get_image(repository_ref._db_id, found_image.docker_image_id)
    assert model_image.id == found_image._db_id
    assert ([pid for pid in reversed(model_image.ancestor_id_list())] ==
            [p._db_id for p in found_image.parents])

    # Try without parents and ensure it raises an exception.
    found_image = registry_model.get_legacy_image(repository_ref, image.docker_image_id,
                                                  include_parents=False)
    with pytest.raises(Exception):
      assert not found_image.parents

  assert found_tags

  unknown = registry_model.get_legacy_image(repository_ref, 'unknown', include_parents=True)
  assert unknown is None


def test_manifest_labels(registry_model):
  repo = model.repository.get_repository('devtable', 'simple')
  repository_ref = RepositoryReference.for_repo_obj(repo)
  found_tag = registry_model.find_matching_tag(repository_ref, ['latest'])
  found_manifest = registry_model.get_manifest_for_tag(found_tag)

  # Create a new label.
  created = registry_model.create_manifest_label(found_manifest, 'foo', 'bar', 'api')
  assert created.key == 'foo'
  assert created.value == 'bar'
  assert created.source_type_name == 'api'
  assert created.media_type_name == 'text/plain'

  # Ensure we can look it up.
  assert registry_model.get_manifest_label(found_manifest, created.uuid) == created

  # Ensure it is in our list of labels.
  assert created in registry_model.list_manifest_labels(found_manifest)
  assert created in registry_model.list_manifest_labels(found_manifest, key_prefix='fo')

  # Ensure it is *not* in our filtered list.
  assert created not in registry_model.list_manifest_labels(found_manifest, key_prefix='ba')

  # Delete the label and ensure it is gone.
  assert registry_model.delete_manifest_label(found_manifest, created.uuid)
  assert registry_model.get_manifest_label(found_manifest, created.uuid) is None
  assert created not in registry_model.list_manifest_labels(found_manifest)


def test_manifest_label_handlers(registry_model):
  repo = model.repository.get_repository('devtable', 'simple')
  repository_ref = RepositoryReference.for_repo_obj(repo)
  found_tag = registry_model.get_repo_tag(repository_ref, 'latest')
  found_manifest = registry_model.get_manifest_for_tag(found_tag)

  # Ensure the tag has no expiration.
  assert found_tag.lifetime_end_ts is None

  # Create a new label with an expires-after.
  registry_model.create_manifest_label(found_manifest, 'quay.expires-after', '2h', 'api')

  # Ensure the tag now has an expiration.
  updated_tag = registry_model.get_repo_tag(repository_ref, 'latest')
  assert updated_tag.lifetime_end_ts == (updated_tag.lifetime_start_ts + (60 * 60 * 2))


def test_batch_labels(registry_model):
  repo = model.repository.get_repository('devtable', 'history')
  repository_ref = RepositoryReference.for_repo_obj(repo)
  found_tag = registry_model.find_matching_tag(repository_ref, ['latest'])
  found_manifest = registry_model.get_manifest_for_tag(found_tag)

  with registry_model.batch_create_manifest_labels(found_manifest) as add_label:
    add_label('foo', '1', 'api')
    add_label('bar', '2', 'api')
    add_label('baz', '3', 'api')

  # Ensure we can look them up.
  assert len(registry_model.list_manifest_labels(found_manifest)) == 3


@pytest.mark.parametrize('repo_namespace, repo_name', [
  ('devtable', 'simple'),
  ('devtable', 'complex'),
  ('devtable', 'history'),
  ('buynlarge', 'orgrepo'),
])
def test_repository_tags(repo_namespace, repo_name, registry_model):
  repository_ref = registry_model.lookup_repository(repo_namespace, repo_name)
  tags = registry_model.list_all_active_repository_tags(repository_ref, include_legacy_images=True)
  assert len(tags)

  tags_map = registry_model.get_legacy_tags_map(repository_ref, storage)

  for tag in tags:
    found_tag = registry_model.get_repo_tag(repository_ref, tag.name, include_legacy_image=True)
    assert found_tag == tag

    if found_tag.legacy_image is None:
      continue

    found_image = registry_model.get_legacy_image(repository_ref,
                                                  found_tag.legacy_image.docker_image_id)
    assert found_image == found_tag.legacy_image
    assert tag.name in tags_map
    assert tags_map[tag.name] == found_image.docker_image_id


@pytest.mark.parametrize('namespace, name, expected_tag_count, has_expired', [
  ('devtable', 'simple', 2, False),
  ('devtable', 'history', 2, True),
  ('devtable', 'gargantuan', 8, False),
  ('public', 'publicrepo', 1, False),
])
def test_repository_tag_history(namespace, name, expected_tag_count, has_expired, registry_model):
  # Pre-cache media type loads to ensure consistent query count.
  Manifest.media_type.get_name(1)

  repository_ref = registry_model.lookup_repository(namespace, name)
  with assert_query_count(2):
    history, has_more = registry_model.list_repository_tag_history(repository_ref)
    assert not has_more
    assert len(history) == expected_tag_count

    for tag in history:
      # Retrieve the manifest to ensure it doesn't issue extra queries.
      tag.manifest

  if has_expired:
    # Ensure the latest tag is marked expired, since there is an expired one.
    with assert_query_count(1):
      assert registry_model.has_expired_tag(repository_ref, 'latest')


@pytest.mark.parametrize('repositories, expected_tag_count', [
  ([], 0),
  ([('devtable', 'simple'), ('devtable', 'building')], 1),
])
def test_get_most_recent_tag_lifetime_start(repositories, expected_tag_count, registry_model):
  last_modified_map = registry_model.get_most_recent_tag_lifetime_start(
    [registry_model.lookup_repository(name, namespace) for name, namespace in repositories]
  )

  assert len(last_modified_map) == expected_tag_count
  for repo_id, last_modified in last_modified_map.items():
    tag = registry_model.get_most_recent_tag(RepositoryReference.for_id(repo_id))
    assert last_modified == tag.lifetime_start_ms / 1000


@pytest.mark.parametrize('repo_namespace, repo_name', [
  ('devtable', 'simple'),
  ('devtable', 'complex'),
  ('devtable', 'history'),
  ('buynlarge', 'orgrepo'),
])
@pytest.mark.parametrize('via_manifest', [
  False,
  True,
])
def test_delete_tags(repo_namespace, repo_name, via_manifest, registry_model):
  repository_ref = registry_model.lookup_repository(repo_namespace, repo_name)
  tags = registry_model.list_all_active_repository_tags(repository_ref)
  assert len(tags)

  # Save history before the deletions.
  previous_history, _ = registry_model.list_repository_tag_history(repository_ref, size=1000)
  assert len(previous_history) >= len(tags)

  # Delete every tag in the repository.
  for tag in tags:
    if via_manifest:
      assert registry_model.delete_tag(repository_ref, tag.name)
    else:
      manifest = registry_model.get_manifest_for_tag(tag)
      if manifest is not None:
        assert registry_model.delete_tags_for_manifest(manifest)

    # Make sure the tag is no longer found.
    # TODO: Uncomment once we're done with the SplitModel.
    #with assert_query_count(1):
    found_tag = registry_model.get_repo_tag(repository_ref, tag.name, include_legacy_image=True)
    assert found_tag is None

  # Ensure all tags have been deleted.
  tags = registry_model.list_all_active_repository_tags(repository_ref)
  assert not len(tags)

  # Ensure that the tags all live in history.
  history, _ = registry_model.list_repository_tag_history(repository_ref, size=1000)
  assert len(history) == len(previous_history)


@pytest.mark.parametrize('use_manifest', [
  True,
  False,
])
def test_retarget_tag_history(use_manifest, registry_model):
  repository_ref = registry_model.lookup_repository('devtable', 'history')
  history, _ = registry_model.list_repository_tag_history(repository_ref)

  if use_manifest:
    manifest_or_legacy_image = registry_model.lookup_manifest_by_digest(repository_ref,
                                                                        history[0].manifest_digest,
                                                                        allow_dead=True)
  else:
    manifest_or_legacy_image = history[0].legacy_image

  # Retarget the tag.
  assert manifest_or_legacy_image
  updated_tag = registry_model.retarget_tag(repository_ref, 'latest', manifest_or_legacy_image,
                                            storage, docker_v2_signing_key, is_reversion=True)

  # Ensure the tag has changed targets.
  if use_manifest:
    assert updated_tag.manifest_digest == manifest_or_legacy_image.digest
  else:
    assert updated_tag.legacy_image == manifest_or_legacy_image

  # Ensure history has been updated.
  new_history, _ = registry_model.list_repository_tag_history(repository_ref)
  assert len(new_history) == len(history) + 1


def test_retarget_tag_schema1(oci_model):
  repository_ref = oci_model.lookup_repository('devtable', 'simple')
  latest_tag = oci_model.get_repo_tag(repository_ref, 'latest')
  manifest = oci_model.get_manifest_for_tag(latest_tag)

  existing_parsed = manifest.get_parsed_manifest()

  # Retarget a new tag to the manifest.
  updated_tag = oci_model.retarget_tag(repository_ref, 'somenewtag', manifest, storage,
                                       docker_v2_signing_key)
  assert updated_tag
  assert updated_tag.name == 'somenewtag'

  updated_manifest = oci_model.get_manifest_for_tag(updated_tag)
  parsed = updated_manifest.get_parsed_manifest()
  assert parsed.namespace == 'devtable'
  assert parsed.repo_name == 'simple'
  assert parsed.tag == 'somenewtag'

  assert parsed.layers == existing_parsed.layers

  # Ensure the tag has changed targets.
  assert oci_model.get_repo_tag(repository_ref, 'somenewtag') == updated_tag


def test_change_repository_tag_expiration(registry_model):
  repository_ref = registry_model.lookup_repository('devtable', 'simple')
  tag = registry_model.get_repo_tag(repository_ref, 'latest')
  assert tag.lifetime_end_ts is None

  new_datetime = datetime.utcnow() + timedelta(days=2)
  previous, okay = registry_model.change_repository_tag_expiration(tag, new_datetime)

  assert okay
  assert previous is None

  tag = registry_model.get_repo_tag(repository_ref, 'latest')
  assert tag.lifetime_end_ts is not None


@pytest.mark.parametrize('repo_namespace, repo_name, expected_non_empty', [
  ('devtable', 'simple', []),
  ('devtable', 'complex', ['prod', 'v2.0']),
  ('devtable', 'history', ['latest']),
  ('buynlarge', 'orgrepo', []),
  ('devtable', 'gargantuan', ['v2.0', 'v3.0', 'v4.0', 'v5.0', 'v6.0']),
])
def test_get_legacy_images_owned_by_tag(repo_namespace, repo_name, expected_non_empty,
                                        registry_model):
  repository_ref = registry_model.lookup_repository(repo_namespace, repo_name)
  tags = registry_model.list_all_active_repository_tags(repository_ref)
  assert len(tags)

  non_empty = set()
  for tag in tags:
    if registry_model.get_legacy_images_owned_by_tag(tag):
      non_empty.add(tag.name)

  assert non_empty == set(expected_non_empty)


def test_get_security_status(registry_model):
  repository_ref = registry_model.lookup_repository('devtable', 'simple')
  tags = registry_model.list_all_active_repository_tags(repository_ref, include_legacy_images=True)
  assert len(tags)

  for tag in tags:
    assert registry_model.get_security_status(tag.legacy_image)
    registry_model.reset_security_status(tag.legacy_image)
    assert registry_model.get_security_status(tag.legacy_image)


@pytest.fixture()
def clear_rows(initialized_db):
  # Remove all new-style rows so we can backfill.
  TagToRepositoryTag.delete().execute()
  Tag.delete().execute()
  TagManifestLabelMap.delete().execute()
  ManifestLabel.delete().execute()
  ManifestBlob.delete().execute()
  ManifestLegacyImage.delete().execute()
  TagManifestToManifest.delete().execute()
  Manifest.delete().execute()
  TagManifestLabel.delete().execute()
  TagManifest.delete().execute()


@pytest.mark.parametrize('repo_namespace, repo_name', [
  ('devtable', 'simple'),
  ('devtable', 'complex'),
  ('devtable', 'history'),
  ('buynlarge', 'orgrepo'),
])
def test_backfill_manifest_for_tag(repo_namespace, repo_name, clear_rows, pre_oci_model):
  repository_ref = pre_oci_model.lookup_repository(repo_namespace, repo_name)
  tags, has_more = pre_oci_model.list_repository_tag_history(repository_ref, size=2500)
  assert tags
  assert not has_more

  for tag in tags:
    assert not tag.manifest_digest
    assert pre_oci_model.backfill_manifest_for_tag(tag)

  tags, _ = pre_oci_model.list_repository_tag_history(repository_ref)
  assert tags
  for tag in tags:
    assert tag.manifest_digest

    manifest = pre_oci_model.get_manifest_for_tag(tag)
    assert manifest

    legacy_image = pre_oci_model.get_legacy_image(repository_ref, tag.legacy_image.docker_image_id,
                                                  include_parents=True)

    parsed_manifest = manifest.get_parsed_manifest()
    assert parsed_manifest.leaf_layer_v1_image_id == legacy_image.docker_image_id
    assert parsed_manifest.parent_image_ids == {p.docker_image_id for p in legacy_image.parents}


@pytest.mark.parametrize('repo_namespace, repo_name', [
  ('devtable', 'simple'),
  ('devtable', 'complex'),
  ('devtable', 'history'),
  ('buynlarge', 'orgrepo'),
])
def test_backfill_manifest_on_lookup(repo_namespace, repo_name, clear_rows, pre_oci_model):
  repository_ref = pre_oci_model.lookup_repository(repo_namespace, repo_name)
  tags = pre_oci_model.list_all_active_repository_tags(repository_ref)
  assert tags

  for tag in tags:
    assert not tag.manifest_digest
    assert not pre_oci_model.get_manifest_for_tag(tag)

    manifest = pre_oci_model.get_manifest_for_tag(tag, backfill_if_necessary=True)
    assert manifest

    updated_tag = pre_oci_model.get_repo_tag(repository_ref, tag.name)
    assert updated_tag.manifest_digest == manifest.digest


@pytest.mark.parametrize('namespace, expect_enabled', [
  ('devtable', True),
  ('buynlarge', True),

  ('disabled', False),
])
def test_is_namespace_enabled(namespace, expect_enabled, registry_model):
  assert registry_model.is_namespace_enabled(namespace) == expect_enabled


@pytest.mark.parametrize('repo_namespace, repo_name', [
  ('devtable', 'simple'),
  ('devtable', 'complex'),
  ('devtable', 'history'),
  ('buynlarge', 'orgrepo'),
])
def test_layers_and_blobs(repo_namespace, repo_name, registry_model):
  repository_ref = registry_model.lookup_repository(repo_namespace, repo_name)
  tags = registry_model.list_all_active_repository_tags(repository_ref)
  assert tags

  for tag in tags:
    manifest = registry_model.get_manifest_for_tag(tag)
    assert manifest

    parsed = manifest.get_parsed_manifest()
    assert parsed

    layers = registry_model.list_parsed_manifest_layers(repository_ref, parsed, storage)
    assert layers

    layers = registry_model.list_parsed_manifest_layers(repository_ref, parsed, storage,
                                                        include_placements=True)
    assert layers

    for index, manifest_layer in enumerate(layers):
      assert manifest_layer.blob.storage_path
      assert manifest_layer.blob.placements

      repo_blob = registry_model.get_repo_blob_by_digest(repository_ref, manifest_layer.blob.digest)
      assert repo_blob.digest == manifest_layer.blob.digest

      assert manifest_layer.estimated_size(1) is not None
      assert isinstance(manifest_layer.layer_info, ManifestImageLayer)

    blobs = registry_model.get_manifest_local_blobs(manifest, include_placements=True)
    assert {b.digest for b in blobs} == set(parsed.local_blob_digests)


def test_manifest_remote_layers(oci_model):
  # Create a config blob for testing.
  config_json = json.dumps({
    'config': {},
    "rootfs": {
      "type": "layers",
      "diff_ids": []
    },
    "history": [
      {
        "created": "2018-04-03T18:37:09.284840891Z",
        "created_by": "do something",
      },
    ],
  })

  app_config = {'TESTING': True}
  repository_ref = oci_model.lookup_repository('devtable', 'simple')
  with upload_blob(repository_ref, storage, BlobUploadSettings(500, 500, 500)) as upload:
    upload.upload_chunk(app_config, BytesIO(config_json))
    blob = upload.commit_to_blob(app_config)

  # Create the manifest in the repo.
  builder = DockerSchema2ManifestBuilder()
  builder.set_config_digest(blob.digest, blob.compressed_size)
  builder.add_layer('sha256:abcd', 1234, urls=['http://hello/world'])
  manifest = builder.build()

  created_manifest, _ = oci_model.create_manifest_and_retarget_tag(repository_ref, manifest,
                                                                   'sometag', storage)
  assert created_manifest

  layers = oci_model.list_parsed_manifest_layers(repository_ref,
                                                 created_manifest.get_parsed_manifest(),
                                                 storage)
  assert len(layers) == 1
  assert layers[0].layer_info.is_remote
  assert layers[0].layer_info.urls == ['http://hello/world']
  assert layers[0].blob is None


def test_derived_image(registry_model):
  # Clear all existing derived storage.
  DerivedStorageForImage.delete().execute()

  repository_ref = registry_model.lookup_repository('devtable', 'simple')
  tag = registry_model.get_repo_tag(repository_ref, 'latest')
  manifest = registry_model.get_manifest_for_tag(tag)

  # Ensure the squashed image doesn't exist.
  assert registry_model.lookup_derived_image(manifest, 'squash', storage, {}) is None

  # Create a new one.
  squashed = registry_model.lookup_or_create_derived_image(manifest, 'squash',
                                                           'local_us', storage, {})
  assert registry_model.lookup_or_create_derived_image(manifest, 'squash',
                                                       'local_us', storage, {}) == squashed
  assert squashed.unique_id

  # Check and set the size.
  assert squashed.blob.compressed_size is None
  registry_model.set_derived_image_size(squashed, 1234)

  found = registry_model.lookup_derived_image(manifest, 'squash', storage, {})
  assert found.blob.compressed_size == 1234
  assert found.unique_id == squashed.unique_id

  # Ensure its returned now.
  assert found == squashed

  # Ensure different metadata results in a different derived image.
  found = registry_model.lookup_derived_image(manifest, 'squash', storage, {'foo': 'bar'})
  assert found is None

  squashed_foo = registry_model.lookup_or_create_derived_image(manifest, 'squash', 'local_us',
                                                               storage, {'foo': 'bar'})
  assert squashed_foo != squashed

  found = registry_model.lookup_derived_image(manifest, 'squash', storage, {'foo': 'bar'})
  assert found == squashed_foo

  assert squashed.unique_id != squashed_foo.unique_id

  # Lookup with placements.
  squashed = registry_model.lookup_or_create_derived_image(manifest, 'squash', 'local_us',
                                                           storage, {}, include_placements=True)
  assert squashed.blob.placements

  # Delete the derived image.
  registry_model.delete_derived_image(squashed)
  assert registry_model.lookup_derived_image(manifest, 'squash', storage, {}) is None


def test_derived_image_signatures(registry_model):
  repository_ref = registry_model.lookup_repository('devtable', 'simple')
  tag = registry_model.get_repo_tag(repository_ref, 'latest')
  manifest = registry_model.get_manifest_for_tag(tag)

  derived = registry_model.lookup_derived_image(manifest, 'squash', storage, {})
  assert derived

  signature = registry_model.get_derived_image_signature(derived, 'gpg2')
  assert signature is None

  registry_model.set_derived_image_signature(derived, 'gpg2', 'foo')
  assert registry_model.get_derived_image_signature(derived, 'gpg2') == 'foo'


def test_derived_image_for_manifest_list(oci_model):
  # Clear all existing derived storage.
  DerivedStorageForImage.delete().execute()

  # Create a config blob for testing.
  config_json = json.dumps({
    'config': {},
    "rootfs": {
      "type": "layers",
      "diff_ids": []
    },
    "history": [
      {
        "created": "2018-04-03T18:37:09.284840891Z",
        "created_by": "do something",
      },
    ],
  })

  app_config = {'TESTING': True}
  repository_ref = oci_model.lookup_repository('devtable', 'simple')
  with upload_blob(repository_ref, storage, BlobUploadSettings(500, 500, 500)) as upload:
    upload.upload_chunk(app_config, BytesIO(config_json))
    blob = upload.commit_to_blob(app_config)

  # Create the manifest in the repo.
  builder = DockerSchema2ManifestBuilder()
  builder.set_config_digest(blob.digest, blob.compressed_size)
  builder.add_layer(blob.digest, blob.compressed_size)
  amd64_manifest = builder.build()

  oci_model.create_manifest_and_retarget_tag(repository_ref, amd64_manifest, 'submanifest', storage)

  # Create a manifest list, pointing to at least one amd64+linux manifest.
  builder = DockerSchema2ManifestListBuilder()
  builder.add_manifest(amd64_manifest, 'amd64', 'linux')
  manifestlist = builder.build()

  oci_model.create_manifest_and_retarget_tag(repository_ref, manifestlist, 'listtag', storage)
  manifest = oci_model.get_manifest_for_tag(oci_model.get_repo_tag(repository_ref, 'listtag'))
  assert manifest
  assert manifest.get_parsed_manifest().is_manifest_list

  # Ensure the squashed image doesn't exist.
  assert oci_model.lookup_derived_image(manifest, 'squash', storage, {}) is None

  # Create a new one.
  squashed = oci_model.lookup_or_create_derived_image(manifest, 'squash', 'local_us', storage, {})
  assert squashed.unique_id
  assert oci_model.lookup_or_create_derived_image(manifest, 'squash',
                                                  'local_us', storage, {}) == squashed

  # Perform lookup.
  assert oci_model.lookup_derived_image(manifest, 'squash', storage, {}) == squashed


def test_torrent_info(registry_model):
  # Remove all existing info.
  TorrentInfo.delete().execute()

  repository_ref = registry_model.lookup_repository('devtable', 'simple')
  tag = registry_model.get_repo_tag(repository_ref, 'latest')
  manifest = registry_model.get_manifest_for_tag(tag)

  blobs = registry_model.get_manifest_local_blobs(manifest)
  assert blobs

  assert registry_model.get_torrent_info(blobs[0]) is None
  registry_model.set_torrent_info(blobs[0], 2, 'foo')

  # Set it again exactly, which should be a no-op.
  registry_model.set_torrent_info(blobs[0], 2, 'foo')

  # Check the information we've set.
  torrent_info = registry_model.get_torrent_info(blobs[0])
  assert torrent_info is not None
  assert torrent_info.piece_length == 2
  assert torrent_info.pieces == 'foo'

  # Try setting it again. Nothing should happen.
  registry_model.set_torrent_info(blobs[0], 3, 'bar')

  torrent_info = registry_model.get_torrent_info(blobs[0])
  assert torrent_info is not None
  assert torrent_info.piece_length == 2
  assert torrent_info.pieces == 'foo'


def test_blob_uploads(registry_model):
  repository_ref = registry_model.lookup_repository('devtable', 'simple')

  blob_upload = registry_model.create_blob_upload(repository_ref, str(uuid.uuid4()),
                                                 'local_us', {'some': 'metadata'})
  assert blob_upload
  assert blob_upload.storage_metadata == {'some': 'metadata'}
  assert blob_upload.location_name == 'local_us'

  # Ensure we can find the blob upload.
  assert registry_model.lookup_blob_upload(repository_ref, blob_upload.upload_id) == blob_upload

  # Update and ensure the changes are saved.
  assert registry_model.update_blob_upload(blob_upload, 1, 'the-pieces_hash',
                                          blob_upload.piece_sha_state,
                                          {'new': 'metadata'}, 2, 3,
                                          blob_upload.sha_state)

  updated = registry_model.lookup_blob_upload(repository_ref, blob_upload.upload_id)
  assert updated
  assert updated.uncompressed_byte_count == 1
  assert updated.piece_hashes == 'the-pieces_hash'
  assert updated.storage_metadata == {'new': 'metadata'}
  assert updated.byte_count == 2
  assert updated.chunk_count == 3

  # Delete the upload.
  registry_model.delete_blob_upload(blob_upload)

  # Ensure it can no longer be found.
  assert not registry_model.lookup_blob_upload(repository_ref, blob_upload.upload_id)


def test_commit_blob_upload(registry_model):
  repository_ref = registry_model.lookup_repository('devtable', 'simple')
  blob_upload = registry_model.create_blob_upload(repository_ref, str(uuid.uuid4()),
                                                 'local_us', {'some': 'metadata'})

  # Commit the blob upload and make sure it is written as a blob.
  digest = 'sha256:' + hashlib.sha256('hello').hexdigest()
  blob = registry_model.commit_blob_upload(blob_upload, digest, 60)
  assert blob.digest == digest

  # Ensure the upload can no longer be found.
  assert not registry_model.lookup_blob_upload(repository_ref, blob_upload.upload_id)


# TODO: Re-enable for OCI model once we have a new table for temporary blobs.
def test_mount_blob_into_repository(pre_oci_model):
  repository_ref = pre_oci_model.lookup_repository('devtable', 'simple')
  latest_tag = pre_oci_model.get_repo_tag(repository_ref, 'latest')
  manifest = pre_oci_model.get_manifest_for_tag(latest_tag)

  target_repository_ref = pre_oci_model.lookup_repository('devtable', 'complex')

  blobs = pre_oci_model.get_manifest_local_blobs(manifest, include_placements=True)
  assert blobs

  for blob in blobs:
    # Ensure the blob doesn't exist under the repository.
    assert not pre_oci_model.get_repo_blob_by_digest(target_repository_ref, blob.digest)

    # Mount the blob into the repository.
    assert pre_oci_model.mount_blob_into_repository(blob, target_repository_ref, 60)

    # Ensure it now exists.
    found = pre_oci_model.get_repo_blob_by_digest(target_repository_ref, blob.digest)
    assert found == blob


class SomeException(Exception):
  pass


def test_get_cached_repo_blob(registry_model):
  model_cache = InMemoryDataModelCache()

  repository_ref = registry_model.lookup_repository('devtable', 'simple')
  latest_tag = registry_model.get_repo_tag(repository_ref, 'latest')
  manifest = registry_model.get_manifest_for_tag(latest_tag)

  blobs = registry_model.get_manifest_local_blobs(manifest, include_placements=True)
  assert blobs

  blob = blobs[0]

  # Load a blob to add it to the cache.
  found = registry_model.get_cached_repo_blob(model_cache, 'devtable', 'simple', blob.digest)
  assert found.digest == blob.digest
  assert found.uuid == blob.uuid
  assert found.compressed_size == blob.compressed_size
  assert found.uncompressed_size == blob.uncompressed_size
  assert found.uploading == blob.uploading
  assert found.placements == blob.placements

  # Disconnect from the database by overwriting the connection.
  def fail(x, y):
    raise SomeException('Not connected!')

  with patch('data.registry_model.registry_pre_oci_model.model.blob.get_repository_blob_by_digest',
             fail):
    with patch('data.registry_model.registry_oci_model.model.oci.blob.get_repository_blob_by_digest',
              fail):
      # Make sure we can load again, which should hit the cache.
      cached = registry_model.get_cached_repo_blob(model_cache, 'devtable', 'simple', blob.digest)
      assert cached.digest == blob.digest
      assert cached.uuid == blob.uuid
      assert cached.compressed_size == blob.compressed_size
      assert cached.uncompressed_size == blob.uncompressed_size
      assert cached.uploading == blob.uploading
      assert cached.placements == blob.placements

      # Try another blob, which should fail since the DB is not connected and the cache
      # does not contain the blob.
      with pytest.raises(SomeException):
        registry_model.get_cached_repo_blob(model_cache, 'devtable', 'simple', 'some other digest')


def test_create_manifest_and_retarget_tag(registry_model):
  repository_ref = registry_model.lookup_repository('devtable', 'simple')
  latest_tag = registry_model.get_repo_tag(repository_ref, 'latest', include_legacy_image=True)
  manifest = registry_model.get_manifest_for_tag(latest_tag).get_parsed_manifest()

  builder = DockerSchema1ManifestBuilder('devtable', 'simple', 'anothertag')
  builder.add_layer(manifest.blob_digests[0],
                    '{"id": "%s"}' % latest_tag.legacy_image.docker_image_id)
  sample_manifest = builder.build(docker_v2_signing_key)
  assert sample_manifest is not None

  another_manifest, tag = registry_model.create_manifest_and_retarget_tag(repository_ref,
                                                                          sample_manifest,
                                                                          'anothertag',
                                                                          storage)
  assert another_manifest is not None
  assert tag is not None

  assert tag.name == 'anothertag'
  assert another_manifest.get_parsed_manifest().manifest_dict == sample_manifest.manifest_dict


def test_get_schema1_parsed_manifest(registry_model):
  repository_ref = registry_model.lookup_repository('devtable', 'simple')
  latest_tag = registry_model.get_repo_tag(repository_ref, 'latest', include_legacy_image=True)
  manifest = registry_model.get_manifest_for_tag(latest_tag)
  assert registry_model.get_schema1_parsed_manifest(manifest, '', '', '', storage)


def test_convert_manifest(registry_model):
  repository_ref = registry_model.lookup_repository('devtable', 'simple')
  latest_tag = registry_model.get_repo_tag(repository_ref, 'latest', include_legacy_image=True)
  manifest = registry_model.get_manifest_for_tag(latest_tag)

  mediatypes = DOCKER_SCHEMA1_CONTENT_TYPES
  assert registry_model.convert_manifest(manifest, '', '', '', mediatypes, storage)

  mediatypes = []
  assert registry_model.convert_manifest(manifest, '', '', '', mediatypes, storage) is None


def test_create_manifest_and_retarget_tag_with_labels(registry_model):
  repository_ref = registry_model.lookup_repository('devtable', 'simple')
  latest_tag = registry_model.get_repo_tag(repository_ref, 'latest', include_legacy_image=True)
  manifest = registry_model.get_manifest_for_tag(latest_tag).get_parsed_manifest()

  json_metadata = {
    'id': latest_tag.legacy_image.docker_image_id,
    'config': {
      'Labels': {
        'quay.expires-after': '2w',
      },
    },
  }

  builder = DockerSchema1ManifestBuilder('devtable', 'simple', 'anothertag')
  builder.add_layer(manifest.blob_digests[0], json.dumps(json_metadata))
  sample_manifest = builder.build(docker_v2_signing_key)
  assert sample_manifest is not None

  another_manifest, tag = registry_model.create_manifest_and_retarget_tag(repository_ref,
                                                                          sample_manifest,
                                                                          'anothertag',
                                                                          storage)
  assert another_manifest is not None
  assert tag is not None

  assert tag.name == 'anothertag'
  assert another_manifest.get_parsed_manifest().manifest_dict == sample_manifest.manifest_dict

  # Ensure the labels were applied.
  assert tag.lifetime_end_ms is not None



def _populate_blob(digest):
  location = ImageStorageLocation.get(name='local_us')
  store_blob_record_and_temp_link('devtable', 'simple', digest, location, 1, 120)


def test_known_issue_schema1(registry_model):
  test_dir = os.path.dirname(os.path.abspath(__file__))
  path = os.path.join(test_dir, '../../../image/docker/test/validate_manifest_known_issue.json')
  with open(path, 'r') as f:
    manifest_bytes = f.read()

  manifest = DockerSchema1Manifest(Bytes.for_string_or_unicode(manifest_bytes))

  for blob_digest in manifest.local_blob_digests:
    _populate_blob(blob_digest)

  digest = manifest.digest
  assert digest == 'sha256:44518f5a4d1cb5b7a6347763116fb6e10f6a8563b6c40bb389a0a982f0a9f47a'

  # Create the manifest in the database.
  repository_ref = registry_model.lookup_repository('devtable', 'simple')
  created_manifest, _ = registry_model.create_manifest_and_retarget_tag(repository_ref, manifest,
                                                                        'latest', storage)
  assert created_manifest
  assert created_manifest.digest == manifest.digest
  assert (created_manifest.internal_manifest_bytes.as_encoded_str() ==
          manifest.bytes.as_encoded_str())

  # Look it up again and validate.
  found = registry_model.lookup_manifest_by_digest(repository_ref, manifest.digest, allow_dead=True)
  assert found
  assert found.digest == digest
  assert found.internal_manifest_bytes.as_encoded_str() == manifest.bytes.as_encoded_str()
  assert found.get_parsed_manifest().digest == digest


def test_unicode_emoji(registry_model):
  builder = DockerSchema1ManifestBuilder('devtable', 'simple', 'latest')
  builder.add_layer('sha256:abcde', json.dumps({
    'id': 'someid',
    'author': u'😱',
  }, ensure_ascii=False))

  manifest = builder.build(ensure_ascii=False)
  manifest._validate()

  for blob_digest in manifest.local_blob_digests:
    _populate_blob(blob_digest)

  # Create the manifest in the database.
  repository_ref = registry_model.lookup_repository('devtable', 'simple')
  created_manifest, _ = registry_model.create_manifest_and_retarget_tag(repository_ref, manifest,
                                                                        'latest', storage)
  assert created_manifest
  assert created_manifest.digest == manifest.digest
  assert (created_manifest.internal_manifest_bytes.as_encoded_str() ==
          manifest.bytes.as_encoded_str())

  # Look it up again and validate.
  found = registry_model.lookup_manifest_by_digest(repository_ref, manifest.digest, allow_dead=True)
  assert found
  assert found.digest == manifest.digest
  assert found.internal_manifest_bytes.as_encoded_str() == manifest.bytes.as_encoded_str()
  assert found.get_parsed_manifest().digest == manifest.digest


def test_lookup_active_repository_tags(oci_model):
  repository_ref = oci_model.lookup_repository('devtable', 'simple')
  latest_tag = oci_model.get_repo_tag(repository_ref, 'latest')
  manifest = oci_model.get_manifest_for_tag(latest_tag)

  tag_count = 500

  # Create a bunch of tags.
  tags_expected = set()
  for index in range(0, tag_count):
    tags_expected.add('somenewtag%s' % index)
    oci_model.retarget_tag(repository_ref, 'somenewtag%s' % index, manifest, storage,
                           docker_v2_signing_key)

  assert tags_expected

  # List the tags.
  tags_found = set()
  tag_id = None
  while True:
    tags = oci_model.lookup_active_repository_tags(repository_ref, tag_id, 11)
    assert len(tags) <= 11
    for tag in tags[0:10]:
      assert tag.name not in tags_found
      if tag.name in tags_expected:
        tags_found.add(tag.name)
        tags_expected.remove(tag.name)

    if len(tags) < 11:
      break

    tag_id = tags[10].id

  # Make sure we've found all the tags.
  assert tags_found
  assert not tags_expected


def test_yield_tags_for_vulnerability_notification(registry_model):
  repository_ref = registry_model.lookup_repository('devtable', 'complex')

  # Check for all legacy images under the tags and ensure not raised because
  # no notification is yet registered.
  for tag in registry_model.list_all_active_repository_tags(repository_ref,
                                                            include_legacy_images=True):
    image = registry_model.get_legacy_image(repository_ref, tag.legacy_image.docker_image_id,
                                            include_blob=True)
    pairs = [(image.docker_image_id, image.blob.uuid)]
    results = list(registry_model.yield_tags_for_vulnerability_notification(pairs))
    assert not len(results)

  # Register a notification.
  model.notification.create_repo_notification(repository_ref.id, 'vulnerability_found', 'email',
                                              {}, {})

  # Check again.
  for tag in registry_model.list_all_active_repository_tags(repository_ref,
                                                            include_legacy_images=True):
    image = registry_model.get_legacy_image(repository_ref, tag.legacy_image.docker_image_id,
                                            include_blob=True, include_parents=True)

    # Check for every parent of the image.
    for current in image.parents:
      img = registry_model.get_legacy_image(repository_ref, current.docker_image_id,
                                            include_blob=True)
      pairs = [(img.docker_image_id, img.blob.uuid)]
      results = list(registry_model.yield_tags_for_vulnerability_notification(pairs))
      assert len(results) > 0
      assert tag.name in {t.name for t in results}

    # Check for the image itself.
    pairs = [(image.docker_image_id, image.blob.uuid)]
    results = list(registry_model.yield_tags_for_vulnerability_notification(pairs))
    assert len(results) > 0
    assert tag.name in {t.name for t in results}