// Copyright 2017 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

@TestOn('vm')
library;

import 'dart:io';
import 'dart:convert';

import 'package:crypto/crypto.dart';
import 'package:path/path.dart' as p;
import 'package:pub_semver/pub_semver.dart';
import 'package:pubspec_parse/pubspec_parse.dart';
import 'package:test/test.dart';

import 'package:sass/src/util/map.dart';

import '../tool/grind/generate_deprecations.dart' as deprecations;
import '../tool/grind/synchronize.dart' as synchronize;

/// Tests that double-check that everything in the repo looks sensible.
void main() {
  group(
    "up-to-date generated",
    () {
      group("synchronized file:", () {
        for (var (sourcePath, targetPath) in synchronize.sources.pairs) {
          test(
            targetPath,
            () => _assertChecksumMatches(sourcePath, targetPath),
          );
        }
      });

      test(
        "deprecations",
        () => _assertChecksumMatches(
          deprecations.yamlPath,
          deprecations.dartPath,
        ),
      );
    },
    // Windows sees different bytes than other OSes, possibly because of
    // newline normalization issues.
    testOn: "!windows",
  );

  for (var package in [".", "pkg/sass_api"]) {
    group("in ${p.relative(package)}", () {
      test("pubspec version matches CHANGELOG version", () {
        var pubspec = Pubspec.parse(
          File("$package/pubspec.yaml").readAsStringSync(),
          sourceUrl: p.toUri("$package/pubspec.yaml"),
        );
        expect(
          pubspec.version!.toString(),
          matchesChangelogVersion(_changelogVersion(package)),
        );
      });
    });
  }

  group("in pkg/sass_api", () {
    late Pubspec sassPubspec;
    late Pubspec pkgPubspec;
    setUpAll(() {
      sassPubspec = Pubspec.parse(
        File("pubspec.yaml").readAsStringSync(),
        sourceUrl: Uri.parse("pubspec.yaml"),
      );
      pkgPubspec = Pubspec.parse(
        File("pkg/sass_api/pubspec.yaml").readAsStringSync(),
        sourceUrl: p.toUri("pkg/sass_api/pubspec.yaml"),
      );
    });

    test("depends on the current sass version", () {
      if (_isDevVersion(sassPubspec.version!)) return;

      expect(pkgPubspec.dependencies, contains("sass"));
      var dependency = pkgPubspec.dependencies["sass"]!;
      expect(dependency, isA<HostedDependency>());
      expect(
        (dependency as HostedDependency).version,
        equals(sassPubspec.version),
      );
    });

    test(
      "increments along with the sass version",
      () => _checkVersionIncrementsAlong(
        'sass_api',
        sassPubspec,
        pkgPubspec.version!,
      ),
    );

    test("matches SDK version", () {
      expect(
        pkgPubspec.environment["sdk"],
        equals(sassPubspec.environment["sdk"]),
      );
    });

    test("matches dartdoc version", () {
      expect(
        sassPubspec.devDependencies["dartdoc"],
        equals(pkgPubspec.devDependencies["dartdoc"]),
      );
    });
  });

  group("in pkg/sass-parser", () {
    late Pubspec sassPubspec;
    late Map<String, Object?> packageJson;
    setUpAll(() {
      sassPubspec = Pubspec.parse(
        File("pubspec.yaml").readAsStringSync(),
        sourceUrl: Uri.parse("pubspec.yaml"),
      );
      packageJson =
          json.decode(File("pkg/sass-parser/package.json").readAsStringSync())
              as Map<String, Object?>;
    });

    test(
      "package.json version matches CHANGELOG version",
      () => expect(
        packageJson["version"].toString(),
        matchesChangelogVersion(_changelogVersion("pkg/sass-parser")),
      ),
    );

    test(
      "increments along with the sass version",
      () => _checkVersionIncrementsAlong(
        'sass-parser',
        sassPubspec,
        Version.parse(packageJson["version"] as String),
      ),
    );
  });
}

/// Returns whether [version] is a `-dev` version.
bool _isDevVersion(Version version) =>
    version.preRelease.length == 1 && version.preRelease.first == 'dev';

/// Returns the most recent version in the CHANGELOG for [package].
Version _changelogVersion(String package) {
  var firstLine = const LineSplitter()
      .convert(File("$package/CHANGELOG.md").readAsStringSync())
      .first;
  expect(firstLine, startsWith("## "));
  return Version.parse(firstLine.substring(3));
}

/// Returns a [Matcher] that matches any valid variant of the CHANGELOG version
/// [version] that the package itself can have.
Matcher matchesChangelogVersion(Version version) => anyOf(
      equals(version.toString()),
      version.isPreRelease
          ? equals("${version.nextPatch}-dev")
          : equals("$version-dev"),
    );

/// Verifies that [pkgVersion] loks like it was incremented when the version of
/// the main Sass version was as well.
void _checkVersionIncrementsAlong(
  String pkgName,
  Pubspec sassPubspec,
  Version pkgVersion,
) {
  var sassVersion = sassPubspec.version!;
  if (_isDevVersion(sassVersion)) return;

  expect(
    _isDevVersion(pkgVersion),
    isFalse,
    reason: "sass $sassVersion isn't a dev version but $pkgName $pkgVersion "
        "is",
  );

  if (sassVersion.isPreRelease) {
    expect(
      pkgVersion.isPreRelease,
      isTrue,
      reason: "sass $sassVersion is a pre-release version but $pkgName "
          "$pkgVersion isn't",
    );
  }

  // If only sass's patch version was incremented, there's not a good way
  // to tell whether the sub-package's version was incremented as well
  // because we don't have access to the prior version.
  if (sassVersion.patch != 0) return;

  var pkgMajor = pkgVersion.major;
  var pkgMinor = pkgVersion.minor;
  var pkgPatch = pkgVersion.patch;
  if (pkgMajor == 0) {
    // Before 1.0.0, the semantics of each version number are moved up by one
    // place.
    pkgMajor = pkgMinor;
    pkgMinor = pkgPatch;
    pkgPatch = 0;
  }

  if (sassVersion.minor != 0) {
    expect(
      pkgPatch,
      equals(0),
      reason: "sass minor version was incremented, $pkgName must increment "
          "at least its minor version",
    );
  } else {
    expect(
      pkgMinor,
      equals(0),
      reason: "sass major version was incremented, $pkgName must increment "
          "at its major version as well",
    );
  }
}

/// Throws an error if the checksum in [outputPath] doesn't match the hash of
/// the contents of [inputPath].
void _assertChecksumMatches(String inputPath, String outputPath) {
  var inputText = File(inputPath).readAsStringSync();
  var outputText = File(outputPath).readAsStringSync();
  var checksum = sha1.convert(utf8.encode(inputText));
  if (!outputText.contains('// Checksum: $checksum')) {
    fail(
      '$outputPath is out-of-date.\n'
      'Run `dart run grinder` to update it.',
    );
  }
}
