package Test::CPAN::Health::Check::MetaJSON;

use strict;
use warnings;
use autodie qw(:all);

use Carp qw(croak carp);
use Readonly;
use Params::Validate::Strict qw(validate_strict);

use parent 'Test::CPAN::Health::Check';

our $VERSION = '0.1.0';

Readonly::Scalar my $SCORE_JSON_COMPLETE    => 100;
Readonly::Scalar my $SCORE_YML_COMPLETE     =>  70;
Readonly::Scalar my $SCORE_MYMETA_COMPLETE  =>  50;
Readonly::Scalar my $SCORE_JSON_INCOMPLETE  =>  40;
Readonly::Scalar my $SCORE_YML_INCOMPLETE   =>  25;
Readonly::Scalar my $SCORE_MYMETA_INCOMPLETE =>  15;

=head1 NAME

Test::CPAN::Health::Check::MetaJSON - Check that META.json is present, valid, and complete

=head1 SYNOPSIS

    use Test::CPAN::Health::Check::MetaJSON;

    my $check  = Test::CPAN::Health::Check::MetaJSON->new;
    my $result = $check->run($dist);

    printf "%s: %s\n", $result->status, $result->summary;

=head1 DESCRIPTION

Verifies that the distribution ships a well-formed META.json (preferred) or
META.yml (fallback) and that the required CPAN::Meta v2 fields -- name,
version, abstract, author, license -- are present and non-empty.

C<Distribution-E<gt>meta> searches in order: META.json, META.yml,
MYMETA.json, MYMETA.yml.  MYMETA files are generated by C<perl Makefile.PL>
and serve as a fallback for local checkouts.  Canonical META files are
produced by C<make distdir> (MakeMaker) or C<dzil build> (Dist::Zilla) and
must be committed to the repository separately.

Score matrix:

=over 4

=item * 100 -- META.json present; all required fields populated.

=item *  70 -- Only META.yml present; all required fields populated.

=item *  50 -- Only MYMETA.json or MYMETA.yml present; all required fields populated.

=item *  40 -- META.json present; one or more required fields missing/vague.

=item *  25 -- Only META.yml present; one or more required fields missing/vague.

=item *  15 -- Only MYMETA present; one or more required fields missing/vague.

=item *   0 -- No META or MYMETA file found.

=back

=head1 LIMITATIONS

=over 4

=item * The check reads whichever META file C<Distribution-E<gt>meta> resolves.
It does not independently re-parse the file.

=item * C<abstract> is considered missing when it is the literal string
C<'unknown'>, which Dist::Zilla and MakeMaker sometimes use as a placeholder.

=back

=cut

sub id          { return 'meta_json'                                              }
sub name        { return 'META.json'                                              }
sub description { return 'Checks that META.json is present, valid, and complete'  }
sub weight      { return 5                                                        }
sub category    { return 'packaging'                                              }

=head2 run

=head3 PURPOSE

Determine whether a META.json (or META.yml) exists and contains the minimum
required metadata fields.

=head3 API SPECIFICATION

=head4 INPUT

  dist     Test::CPAN::Health::Distribution  required
  context  Hashref                           optional  prior check results

=head4 OUTPUT

L<Test::CPAN::Health::Result> with check_id C<'meta_json'>.

=head3 MESSAGES

  Code  | Severity | Message                                   | Resolution
  ------+----------+-------------------------------------------+-----------
  MJ001 | FAIL     | No META.json or META.yml found            | Generate META files
  MJ002 | WARN     | META missing required fields: {list}      | Add missing fields
  MJ003 | WARN     | META.yml present but META.json is missing | Generate META.json
  MJ004 | PASS     | META.json is present and complete         |

=head3 FORMAL SPECIFICATION

  -- Z schema (placeholder) --
  MetaJSONOp
  dist     : Distribution
  has_json : Boolean
  meta     : CPAN::Meta | undefined
  missing  : seq String
  -------------------------------------------------------
  meta = undefined => status = fail /\ score = 0
  #missing > 0 /\ has_json  => status = warn /\ score = 50
  #missing > 0 /\ ~has_json => status = warn /\ score = 30
  #missing = 0 /\ ~has_json => status = warn /\ score = 70
  #missing = 0 /\ has_json  => status = pass /\ score = 100

=head3 SIDE EFFECTS

None.  Uses the already-parsed META object from C<$dist->meta>.

=head3 USAGE EXAMPLE

    my $result = Test::CPAN::Health::Check::MetaJSON->new->run($dist);
    print $result->summary;

=cut

sub run {
	my ($self, $dist, $context) = @_;

	croak 'dist must be a Test::CPAN::Health::Distribution'
		unless ref($dist) && $dist->isa('Test::CPAN::Health::Distribution');

	my $flags = _meta_presence_flags($dist);
	my $meta  = $dist->meta;

	unless ($meta) {
		return $self->_result(
			status  => 'fail',
			score   => 0,
			summary => 'No META.json, META.yml, MYMETA.json, or MYMETA.yml found',
			details => [
				'MakeMaker: run "perl Makefile.PL && make distdir" then copy META.json and META.yml from the generated dist directory',
				'Dist::Zilla: run "dzil build" which writes META.json to the working directory automatically',
			],
			data    => { name => $self->name },
		);
	}

	my @missing = _missing_meta_fields($meta);

	if (@missing) {
		return $self->_result(
			status  => 'warn',
			score   => _incomplete_meta_score($flags),
			summary => sprintf('META file is missing required fields: %s', join(', ', @missing)),
			details => [ map { "Add '$_' to META.json" } @missing ],
			data    => {
				name     => $self->name,
				has_json => $flags->{json} ? 1 : 0,
				missing  => \@missing,
			},
		);
	}

	if (!$flags->{meta} && $flags->{mymeta}) {
		my $which = $flags->{myjson} ? 'MYMETA.json' : 'MYMETA.yml';
		return $self->_result(
			status  => 'warn',
			score   => $SCORE_MYMETA_COMPLETE,
			summary => "$which found (generated); ship META.json for CPAN compatibility",
			details => [
				'MakeMaker: run "perl Makefile.PL && make distdir" then copy META.json from the generated dist directory',
				'Dist::Zilla: run "dzil build" which writes META.json to the working directory automatically',
			],
			data    => { name => $self->name, has_json => 0 },
		);
	}

	unless ($flags->{json}) {
		return $self->_result(
			status  => 'warn',
			score   => $SCORE_YML_COMPLETE,
			summary => 'META.yml found but META.json is missing (JSON preferred for tooling compatibility)',
			details => ['Generate META.json alongside META.yml'],
			data    => { name => $self->name, has_json => 0 },
		);
	}

	return $self->_result(
		status  => 'pass',
		score   => $SCORE_JSON_COMPLETE,
		summary => 'META.json is present and contains all required fields',
		data    => { name => $self->name, has_json => 1 },
	);
}

# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------

## no critic (ProhibitUnusedPrivateSubroutines)
sub _meta_presence_flags {
	my ($dist) = @_;
	my $has_json   = defined $dist->file_path('META.json');
	my $has_yml    = defined $dist->file_path('META.yml');
	my $has_myjson = defined $dist->file_path('MYMETA.json');
	my $has_myyml  = defined $dist->file_path('MYMETA.yml');
	return {
		json   => $has_json,
		yml    => $has_yml,
		myjson => $has_myjson,
		myyml  => $has_myyml,
		meta   => $has_json || $has_yml,
		mymeta => $has_myjson || $has_myyml,
	};
}

## no critic (ProhibitUnusedPrivateSubroutines)
sub _missing_meta_fields {
	my ($meta) = @_;
	my @authors  = $meta->author;
	my @licenses = $meta->license;
	my @missing;
	push @missing, 'name'
		unless defined $meta->name && length $meta->name;
	push @missing, 'version'
		unless defined $meta->version && length $meta->version;
	push @missing, 'abstract'
		if !defined $meta->abstract
			|| !length $meta->abstract
			|| $meta->abstract eq 'unknown';
	push @missing, 'author'  unless @authors;
	push @missing, 'license' unless @licenses;
	return @missing;
}

## no critic (ProhibitUnusedPrivateSubroutines)
sub _incomplete_meta_score {
	my ($flags) = @_;
	return $flags->{json} ? $SCORE_JSON_INCOMPLETE
	     : $flags->{yml}  ? $SCORE_YML_INCOMPLETE
	     :                  $SCORE_MYMETA_INCOMPLETE;
}

=head1 AUTHOR

Nigel Horne, C<< <njh at nigelhorne.com> >>

=head1 LICENSE AND COPYRIGHT

Copyright (C) 2025-2026 Nigel Horne.

This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation; either version 2 of the License, or (at your option)
any later version.

=cut

1;
